From 5abde1f2893812b67b5fe0123964b2a519dbd2c1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 12:43:00 -0800 Subject: [PATCH 001/117] Add GSOS#getDiffsForCommit and test --- lib/git-shell-out-strategy.js | 8 ++++++++ test/git-strategies.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 0e8f303d4c3..1addc791b31 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -687,6 +687,14 @@ export default class GitShellOutStrategy { return headCommit; } + async getDiffsForCommit(sha) { + const output = await this.exec([ + 'diff', '--no-prefix', '--no-ext-diff', '--no-renames', `${sha}~`, sha, + ]); + + return parseDiff(output); + } + async getCommits(options = {}) { const {max, ref, includeUnborn} = {max: 1, ref: 'HEAD', includeUnborn: false, ...options}; diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 34a24fb290c..574dceadd46 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -158,6 +158,36 @@ import * as reporterProxy from '../lib/reporter-proxy'; }); }); + describe('getDiffsForCommit(sha)', function() { + it('returns the diff for the specified commit sha', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const git = createTestStrategy(workingDirPath); + + const diffs = await git.getDiffForCommit('18920c90'); + + assertDeepPropertyVals(diffs, [{ + oldPath: 'file.txt', + newPath: 'file.txt', + oldMode: '100644', + newMode: '100644', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 1, + newStartLine: 1, + newLineCount: 1, + heading: '', + lines: [ + '-one', + '+two', + ], + }, + ], + status: 'modified', + }]); + }); + }); + describe('getCommits()', function() { describe('when no commits exist in the repository', function() { it('returns an array with an unborn ref commit when the include unborn option is passed', async function() { From 45d1a45fe4b6549744be2b74a0dcdba3acdf9d8b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 13:07:50 -0800 Subject: [PATCH 002/117] Wire up `getCommit` --- lib/models/repository-states/present.js | 11 ++++++++++- lib/models/repository-states/state.js | 4 ++++ lib/models/repository.js | 1 + test/models/repository.test.js | 11 +++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index a9f9691a2ab..3741647e9a0 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -723,6 +723,15 @@ export default class Present extends State { }); } + getCommit(sha) { + return this.cache.getOrSet(Keys.blob.oneWith(sha), async () => { + const [commitMetaData] = await this.git().getCommits({max: 1, ref: sha}); + // todo: check need for error handling in the case of 0 commit and 1 commit + const fileDiffs = await this.git().getDiffsForCommit(sha); + return {...commitMetaData, fileDiffs}; + }); + } + getRecentCommits(options) { return this.cache.getOrSet(Keys.recentCommits, async () => { const commits = await this.git().getCommits({ref: 'HEAD', ...options}); @@ -1097,7 +1106,7 @@ const Keys = { }, blob: { - oneWith: sha => `blob:${sha}`, + oneWith: sha => new CacheKey(`blob:${sha}`, ['blob']), }, // Common collections of keys and patterns for use with invalidate(). diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 85bb53e3a8f..92961a777ad 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -298,6 +298,10 @@ export default class State { return Promise.resolve(nullCommit); } + getCommit() { + return Promise.resolve(nullCommit); + } + getRecentCommits() { return Promise.resolve([]); } diff --git a/lib/models/repository.js b/lib/models/repository.js index 9c39d827eb1..68202d136e2 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -331,6 +331,7 @@ const delegates = [ 'readFileFromIndex', 'getLastCommit', + 'getCommit', 'getRecentCommits', 'getAuthors', diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 834567e9ca8..2eaac205e5f 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -731,6 +731,17 @@ describe('Repository', function() { }); }); + describe('getCommit(sha)', function() { + it('returns the commit information for the provided sha', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + + console.log(await repo.getCommit('18920c90')); + // TODO ... + }); + }); + describe('undoLastCommit()', function() { it('performs a soft reset', async function() { const workingDirPath = await cloneRepository('multiple-commits'); From dacd2f0a3f3ba1acc2171c5ef2eb860542e9789c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 14:07:12 -0800 Subject: [PATCH 003/117] Add the framework for displaying commit details --- lib/containers/commit-detail-container.js | 42 ++++++ lib/controllers/commit-detail-controller.js | 26 ++++ lib/controllers/root-controller.js | 54 ++++++++ lib/github-package.js | 10 ++ lib/items/commit-detail-item.js | 93 +++++++++++++ lib/views/open-commit-dialog.js | 139 ++++++++++++++++++++ package.json | 3 +- 7 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 lib/containers/commit-detail-container.js create mode 100644 lib/controllers/commit-detail-controller.js create mode 100644 lib/items/commit-detail-item.js create mode 100644 lib/views/open-commit-dialog.js diff --git a/lib/containers/commit-detail-container.js b/lib/containers/commit-detail-container.js new file mode 100644 index 00000000000..b4d91765f69 --- /dev/null +++ b/lib/containers/commit-detail-container.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import yubikiri from 'yubikiri'; + +import ObserveModel from '../views/observe-model'; +import LoadingView from '../views/loading-view'; +import CommitDetailController from '../controllers/commit-detail-controller'; + +export default class CommitDetailContainer extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + sha: PropTypes.string.isRequired, + } + + fetchData = repository => { + return yubikiri({ + commit: repository.getCommit(this.props.sha), + }); + } + + render() { + console.log('container'); + return ( + + {this.renderResult} + + ); + } + + renderResult = data => { + if (this.props.repository.isLoading() || data === null) { + return ; + } + + return ( + + ); + } +} diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js new file mode 100644 index 00000000000..f1245cb904a --- /dev/null +++ b/lib/controllers/commit-detail-controller.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import MultiFilePatchController from './multi-file-patch-controller'; + +export default class CommitDetailController extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + + workspace: PropTypes.object.isRequired, + commands: PropTypes.object.isRequired, + keymaps: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + + destroy: PropTypes.func.isRequired, + } + + render() { + return ( + + ); + } +} diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index ec5d22f536f..a720cba46c1 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -11,12 +11,14 @@ import Panel from '../atom/panel'; import PaneItem from '../atom/pane-item'; import CloneDialog from '../views/clone-dialog'; import OpenIssueishDialog from '../views/open-issueish-dialog'; +import OpenCommitDialog from '../views/open-commit-dialog'; import InitDialog from '../views/init-dialog'; import CredentialDialog from '../views/credential-dialog'; import Commands, {Command} from '../atom/commands'; import GitTimingsView from '../views/git-timings-view'; import ChangedFileItem from '../items/changed-file-item'; import IssueishDetailItem from '../items/issueish-detail-item'; +import CommitDetailItem from '../items/commit-detail-item'; import CommitPreviewItem from '../items/commit-preview-item'; import GitTabItem from '../items/git-tab-item'; import GitHubTabItem from '../items/github-tab-item'; @@ -139,6 +141,7 @@ export default class RootController extends React.Component { + ); } @@ -244,6 +248,22 @@ export default class RootController extends React.Component { ); } + renderOpenCommitDialog() { + if (!this.state.openCommitDialogActive) { + return null; + } + + return ( + + + + ); + } + renderCredentialDialog() { if (this.state.credentialDialogQuery === null) { return null; @@ -362,6 +382,24 @@ export default class RootController extends React.Component { /> )} + + {({itemHolder, params}) => ( + + )} + {({itemHolder, params}) => ( { + this.setState({openCommitDialogActive: true}); + } + showWaterfallDiagnostics() { this.props.workspace.open(GitTimingsView.buildURI()); } @@ -548,6 +590,18 @@ export default class RootController extends React.Component { this.setState({openIssueishDialogActive: false}); } + acceptOpenCommit = ({sha}) => { + const uri = CommitDetailItem.buildURI(sha); + this.setState({openCommitDialogActive: false}); + this.props.workspace.open(uri).then(() => { + addEvent('open-commit-in-pane', {package: 'github', from: 'dialog'}); + }); + } + + cancelOpenCommit = () => { + this.setState({openCommitDialogActive: false}); + } + surfaceFromFileAtPath = (filePath, stagingStatus) => { const gitTab = this.gitTabTracker.getComponent(); return gitTab && gitTab.focusAndSelectStagingItem(filePath, stagingStatus); diff --git a/lib/github-package.js b/lib/github-package.js index 0da56cd648a..a0ed4797100 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -389,6 +389,16 @@ export default class GithubPackage { return item; } + createCommitDetailStub({uri}) { + const item = StubItem.create('git-commit-detail', { + title: 'Commit', + }, uri); + if (this.controller) { + this.rerender(); + } + return item; + } + destroyGitTabItem() { if (this.gitTabStubItem) { this.gitTabStubItem.destroy(); diff --git a/lib/items/commit-detail-item.js b/lib/items/commit-detail-item.js new file mode 100644 index 00000000000..564b637144b --- /dev/null +++ b/lib/items/commit-detail-item.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Emitter} from 'event-kit'; + +import {WorkdirContextPoolPropType} from '../prop-types'; +import CommitDetailContainer from '../containers/commit-detail-container'; +import RefHolder from '../models/ref-holder'; + +export default class CommitDetailItem extends React.Component { + static propTypes = { + workdirContextPool: WorkdirContextPoolPropType.isRequired, + workingDirectory: PropTypes.string.isRequired, + sha: PropTypes.string.isRequired, + + surfaceToCommitDetailButton: PropTypes.func.isRequired, + } + + static uriPattern = 'atom-github://commit-detail?sha={sha}' + + static buildURI(sha) { + return `atom-github://commit-detail?sha=${encodeURIComponent(sha)}`; + } + + constructor(props) { + super(props); + + console.log('item'); + this.emitter = new Emitter(); + this.isDestroyed = false; + this.hasTerminatedPendingState = false; + this.refInitialFocus = new RefHolder(); + } + + terminatePendingState() { + if (!this.hasTerminatedPendingState) { + this.emitter.emit('did-terminate-pending-state'); + this.hasTerminatedPendingState = true; + } + } + + onDidTerminatePendingState(callback) { + return this.emitter.on('did-terminate-pending-state', callback); + } + + destroy = () => { + /* istanbul ignore else */ + if (!this.isDestroyed) { + this.emitter.emit('did-destroy'); + this.isDestroyed = true; + } + } + + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback); + } + + render() { + const repository = this.props.workdirContextPool.getContext(this.props.workingDirectory).getRepository(); + + return ( + + ); + } + + getTitle() { + return `Commit: ${this.props.sha}`; + } + + getIconName() { + return 'git-commit'; + } + + getWorkingDirectory() { + return this.props.workingDirectory; + } + + serialize() { + return { + deserializer: 'CommitDetailStub', + uri: CommitDetailItem.buildURI(this.props.sha), + }; + } + + focus() { + this.refInitialFocus.map(focusable => focusable.focus()); + } +} diff --git a/lib/views/open-commit-dialog.js b/lib/views/open-commit-dialog.js new file mode 100644 index 00000000000..6cd2f04d09e --- /dev/null +++ b/lib/views/open-commit-dialog.js @@ -0,0 +1,139 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {CompositeDisposable} from 'event-kit'; + +import Commands, {Command} from '../atom/commands'; +import {autobind} from '../helpers'; + +// const COMMIT_SHA_REGEX = /^(?:https?:\/\/)?github.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/; + +export default class OpenCommitDialog extends React.Component { + static propTypes = { + commandRegistry: PropTypes.object.isRequired, + didAccept: PropTypes.func, + didCancel: PropTypes.func, + } + + static defaultProps = { + didAccept: () => {}, + didCancel: () => {}, + } + + constructor(props, context) { + super(props, context); + autobind(this, 'accept', 'cancel', 'editorRefs', 'didChangeCommitSha'); + console.log('sdfasdf'); + + this.state = { + cloneDisabled: false, + }; + + this.subs = new CompositeDisposable(); + } + + componentDidMount() { + if (this.commitShaElement) { + setTimeout(() => this.commitShaElement.focus()); + } + } + + render() { + return this.renderDialog(); + } + + renderDialog() { + return ( +
+ + + + +
+ + {this.state.error && {this.state.error}} +
+
+ + +
+
+ ); + } + + accept() { + if (this.getCommitSha().length === 0) { + return; + } + + const parsed = this.parseSha(); + if (!parsed) { + this.setState({ + error: 'That is not a valid commit sha.', + }); + return; + } + const {sha} = parsed; + + this.props.didAccept({sha}); + } + + cancel() { + this.props.didCancel(); + } + + editorRefs(baseName) { + const elementName = `${baseName}Element`; + const modelName = `${baseName}Editor`; + const subName = `${baseName}Subs`; + const changeMethodName = `didChange${baseName[0].toUpperCase()}${baseName.substring(1)}`; + + return element => { + if (!element) { + return; + } + + this[elementName] = element; + const editor = element.getModel(); + if (this[modelName] !== editor) { + this[modelName] = editor; + + if (this[subName]) { + this[subName].dispose(); + this.subs.remove(this[subName]); + } + + this[subName] = editor.onDidChange(this[changeMethodName]); + this.subs.add(this[subName]); + } + }; + } + + didChangeCommitSha() { + this.setState({error: null}); + } + + parseSha() { + const sha = this.getCommitSha(); + // const matches = url.match(ISSUEISH_URL_REGEX); + // if (!matches) { + // return false; + // } + // const [_full, repoOwner, repoName, issueishNumber] = matches; // eslint-disable-line no-unused-vars + return {sha}; + } + + getCommitSha() { + return this.commitShaEditor ? this.commitShaEditor.getText() : ''; + } +} diff --git a/package.json b/package.json index 628a56bc5dd..af60df2ba9d 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,7 @@ "GitDockItem": "createDockItemStub", "GithubDockItem": "createDockItemStub", "FilePatchControllerStub": "createFilePatchControllerStub", - "CommitPreviewStub": "createCommitPreviewStub" + "CommitPreviewStub": "createCommitPreviewStub", + "CommitDetailStub": "createCommitDetailStub" } } From 3f692793e3de8396ffaab4c1d41d983d64bd4e42 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 15:08:30 -0800 Subject: [PATCH 004/117] Get CommitDetailItem to actually render! --- lib/containers/commit-detail-container.js | 2 +- lib/controllers/commit-detail-controller.js | 1 + .../multi-file-patch-controller.js | 6 ++-- lib/controllers/root-controller.js | 5 ++- lib/items/commit-detail-item.js | 7 ++-- lib/models/commit.js | 9 +++++ lib/models/repository-states/present.js | 8 +++-- lib/views/hunk-header-view.js | 10 +++--- lib/views/multi-file-patch-view.js | 36 ++++++++++++------- 9 files changed, 54 insertions(+), 30 deletions(-) diff --git a/lib/containers/commit-detail-container.js b/lib/containers/commit-detail-container.js index b4d91765f69..1c5d3022d87 100644 --- a/lib/containers/commit-detail-container.js +++ b/lib/containers/commit-detail-container.js @@ -28,7 +28,7 @@ export default class CommitDetailContainer extends React.Component { } renderResult = data => { - if (this.props.repository.isLoading() || data === null) { + if (this.props.repository.isLoading() || data === null || !data.commit.isPresent()) { return ; } diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index f1245cb904a..1eac077294b 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -19,6 +19,7 @@ export default class CommitDetailController extends React.Component { render() { return ( ); diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 09944505c58..cdba88def62 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -22,9 +22,9 @@ export default class MultiFilePatchController extends React.Component { config: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, - discardLines: PropTypes.func.isRequired, - undoLastDiscard: PropTypes.func.isRequired, - surface: PropTypes.func.isRequired, + discardLines: PropTypes.func, + undoLastDiscard: PropTypes.func, + surface: PropTypes.func, } constructor(props) { diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index a720cba46c1..c57c7a9d4f7 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -397,6 +397,8 @@ export default class RootController extends React.Component { keymaps={this.props.keymaps} tooltips={this.props.tooltips} config={this.props.config} + + sha={params.sha} /> )}
@@ -591,7 +593,8 @@ export default class RootController extends React.Component { } acceptOpenCommit = ({sha}) => { - const uri = CommitDetailItem.buildURI(sha); + const workdir = this.props.repository.getWorkingDirectoryPath(); + const uri = CommitDetailItem.buildURI(workdir, sha); this.setState({openCommitDialogActive: false}); this.props.workspace.open(uri).then(() => { addEvent('open-commit-in-pane', {package: 'github', from: 'dialog'}); diff --git a/lib/items/commit-detail-item.js b/lib/items/commit-detail-item.js index 564b637144b..90c023129f6 100644 --- a/lib/items/commit-detail-item.js +++ b/lib/items/commit-detail-item.js @@ -15,16 +15,15 @@ export default class CommitDetailItem extends React.Component { surfaceToCommitDetailButton: PropTypes.func.isRequired, } - static uriPattern = 'atom-github://commit-detail?sha={sha}' + static uriPattern = 'atom-github://commit-detail?workdir={workingDirectory}&sha={sha}' - static buildURI(sha) { - return `atom-github://commit-detail?sha=${encodeURIComponent(sha)}`; + static buildURI(workingDirectory, sha) { + return `atom-github://commit-detail?workdir=${encodeURIComponent(workingDirectory)}&sha=${encodeURIComponent(sha)}`; } constructor(props) { super(props); - console.log('item'); this.emitter = new Emitter(); this.isDestroyed = false; this.hasTerminatedPendingState = false; diff --git a/lib/models/commit.js b/lib/models/commit.js index 6bfa738b8d3..f1fbd72a1c2 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -13,6 +13,7 @@ export default class Commit { this.messageSubject = messageSubject; this.messageBody = messageBody; this.unbornRef = unbornRef === UNBORN; + this.multiFileDiff = null; } getSha() { @@ -43,6 +44,14 @@ export default class Commit { return `${this.getMessageSubject()}\n\n${this.getMessageBody()}`.trim(); } + setMultiFileDiff(multiFileDiff) { + this.multiFileDiff = multiFileDiff; + } + + getMultiFileDiff() { + return this.multiFileDiff; + } + isUnbornRef() { return this.unbornRef; } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 3741647e9a0..bc285890810 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -725,10 +725,12 @@ export default class Present extends State { getCommit(sha) { return this.cache.getOrSet(Keys.blob.oneWith(sha), async () => { - const [commitMetaData] = await this.git().getCommits({max: 1, ref: sha}); + const [rawCommitMetadata] = await this.git().getCommits({max: 1, ref: sha}); + const commit = new Commit(rawCommitMetadata); // todo: check need for error handling in the case of 0 commit and 1 commit - const fileDiffs = await this.git().getDiffsForCommit(sha); - return {...commitMetaData, fileDiffs}; + const multiFileDiff = await this.git().getDiffsForCommit(sha).then(buildMultiFilePatch); + commit.setMultiFileDiff(multiFileDiff); + return commit; }); } diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index cb72feb36ac..8ee5e96ddad 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -17,16 +17,16 @@ export default class HunkHeaderView extends React.Component { refTarget: RefHolderPropType.isRequired, hunk: PropTypes.object.isRequired, isSelected: PropTypes.bool.isRequired, - stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, + stagingStatus: PropTypes.oneOf(['unstaged', 'staged']), selectionMode: PropTypes.oneOf(['hunk', 'line']).isRequired, - toggleSelectionLabel: PropTypes.string.isRequired, - discardSelectionLabel: PropTypes.string.isRequired, + toggleSelectionLabel: PropTypes.string, + discardSelectionLabel: PropTypes.string, tooltips: PropTypes.object.isRequired, keymaps: PropTypes.object.isRequired, - toggleSelection: PropTypes.func.isRequired, - discardSelection: PropTypes.func.isRequired, + toggleSelection: PropTypes.func, + discardSelection: PropTypes.func, mouseDown: PropTypes.func.isRequired, }; diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 0ff63f97b39..dc922a39d62 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -18,6 +18,7 @@ import HunkHeaderView from './hunk-header-view'; import RefHolder from '../models/ref-holder'; import ChangedFileItem from '../items/changed-file-item'; import CommitPreviewItem from '../items/commit-preview-item'; +import CommitDetailItem from '../items/commit-detail-item'; import File from '../models/patch/file'; const executableText = { @@ -31,7 +32,7 @@ const BLANK_LABEL = () => NBSP_CHARACTER; export default class MultiFilePatchView extends React.Component { static propTypes = { - stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), isPartiallyStaged: PropTypes.bool, multiFilePatch: MultiFilePatchPropType.isRequired, selectionMode: PropTypes.oneOf(['hunk', 'line']).isRequired, @@ -46,17 +47,17 @@ export default class MultiFilePatchView extends React.Component { tooltips: PropTypes.object.isRequired, config: PropTypes.object.isRequired, - selectedRowsChanged: PropTypes.func.isRequired, + selectedRowsChanged: PropTypes.func, - diveIntoMirrorPatch: PropTypes.func.isRequired, - surface: PropTypes.func.isRequired, - openFile: PropTypes.func.isRequired, - toggleFile: PropTypes.func.isRequired, - toggleRows: PropTypes.func.isRequired, - toggleModeChange: PropTypes.func.isRequired, - toggleSymlinkChange: PropTypes.func.isRequired, - undoLastDiscard: PropTypes.func.isRequired, - discardRows: PropTypes.func.isRequired, + diveIntoMirrorPatch: PropTypes.func, + surface: PropTypes.func, + openFile: PropTypes.func, + toggleFile: PropTypes.func, + toggleRows: PropTypes.func, + toggleModeChange: PropTypes.func, + toggleSymlinkChange: PropTypes.func, + undoLastDiscard: PropTypes.func, + discardRows: PropTypes.func, refInitialFocus: RefHolderPropType, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired, @@ -186,6 +187,15 @@ export default class MultiFilePatchView extends React.Component { } renderCommands() { + if (this.props.itemType === CommitDetailItem) { + return ( + + + + + ); + } + let stageModeCommand = null; let stageSymlinkCommand = null; @@ -205,11 +215,11 @@ export default class MultiFilePatchView extends React.Component { return ( + + - - From 2bdf25a73ce227e96110d57116d81667ad32a581 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 15:13:43 -0800 Subject: [PATCH 005/117] Clean up prop errors in the console --- lib/controllers/commit-detail-controller.js | 11 +++++++---- lib/items/commit-detail-item.js | 2 -- lib/views/file-patch-header-view.js | 5 +++-- lib/views/multi-file-patch-view.js | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index 1eac077294b..035dea47e25 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -14,14 +14,17 @@ export default class CommitDetailController extends React.Component { config: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, + commit: PropTypes.object.isRequired, } render() { return ( - +
+ +
); } } diff --git a/lib/items/commit-detail-item.js b/lib/items/commit-detail-item.js index 90c023129f6..ed8f5f19368 100644 --- a/lib/items/commit-detail-item.js +++ b/lib/items/commit-detail-item.js @@ -11,8 +11,6 @@ export default class CommitDetailItem extends React.Component { workdirContextPool: WorkdirContextPoolPropType.isRequired, workingDirectory: PropTypes.string.isRequired, sha: PropTypes.string.isRequired, - - surfaceToCommitDetailButton: PropTypes.func.isRequired, } static uriPattern = 'atom-github://commit-detail?workdir={workingDirectory}&sha={sha}' diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 8f7db7d8e2b..97a54c81d9a 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -7,11 +7,12 @@ import cx from 'classnames'; import RefHolder from '../models/ref-holder'; import ChangedFileItem from '../items/changed-file-item'; import CommitPreviewItem from '../items/commit-preview-item'; +import CommitDetailItem from '../items/commit-detail-item'; export default class FilePatchHeaderView extends React.Component { static propTypes = { relPath: PropTypes.string.isRequired, - stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), isPartiallyStaged: PropTypes.bool, hasHunks: PropTypes.bool.isRequired, hasUndoHistory: PropTypes.bool, @@ -24,7 +25,7 @@ export default class FilePatchHeaderView extends React.Component { openFile: PropTypes.func.isRequired, toggleFile: PropTypes.func.isRequired, - itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired, + itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, }; constructor(props) { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index dc922a39d62..0c81bd6cfd3 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -60,7 +60,7 @@ export default class MultiFilePatchView extends React.Component { discardRows: PropTypes.func, refInitialFocus: RefHolderPropType, - itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired, + itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, } constructor(props) { From 31ed5f48c3e94b4c4eabdb7f59b0ede60e74f20e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 15:45:22 -0800 Subject: [PATCH 006/117] Display some commit metadata (needs prettifying!) --- lib/controllers/commit-detail-controller.js | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index 035dea47e25..40f6ae4ad79 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -1,8 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {emojify} from 'node-emoji'; +import moment from 'moment'; import MultiFilePatchController from './multi-file-patch-controller'; +const avatarAltText = 'committer avatar'; + export default class CommitDetailController extends React.Component { static propTypes = { repository: PropTypes.object.isRequired, @@ -18,13 +22,45 @@ export default class CommitDetailController extends React.Component { } render() { + const commit = this.props.commit; + // const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; + // const {avatarUrl, name, date} = this.props.item.committer; + return (
+
+

+ {emojify(commit.getMessageSubject())} +
+              {emojify(commit.getMessageBody())}
+

+
+ {/* TODO fix image src */} + {avatarAltText} + + {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} + +
+
+
+ {/* TODO fix href */} + + {commit.getSha()} + +
); } + + humanizeTimeSince(date) { + return moment(date).fromNow(); + } } From c5571490dc1f90ea7d2ae3597c7161ae795010b1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 15:51:53 -0800 Subject: [PATCH 007/117] Render co-authors --- lib/controllers/commit-detail-controller.js | 36 ++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index 40f6ae4ad79..b61cdf2177e 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -36,10 +36,7 @@ export default class CommitDetailController extends React.Component {
{/* TODO fix image src */} - {avatarAltText} + {this.renderAuthors()} {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} @@ -63,4 +60,35 @@ export default class CommitDetailController extends React.Component { humanizeTimeSince(date) { return moment(date).fromNow(); } + + renderAuthor(email) { + const match = email.match(/^(\d+)\+[^@]+@users.noreply.github.com$/); + + let avatarUrl; + if (match) { + avatarUrl = 'https://avatars.githubusercontent.com/u/' + match[1] + '?s=32'; + } else { + avatarUrl = 'https://avatars.githubusercontent.com/u/e?email=' + encodeURIComponent(email) + '&s=32'; + } + + return ( + {`${email}'s + ); + } + + renderAuthors() { + const coAuthorEmails = this.props.commit.getCoAuthors().map(author => author.email); + const authorEmails = [this.props.commit.getAuthorEmail(), ...coAuthorEmails]; + + return ( + + {authorEmails.map(this.renderAuthor)} + + ); + } } From fffd2cba2a3187b198199df9698cffad70bbc69c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 15:57:37 -0800 Subject: [PATCH 008/117] Render author number count if co-authors present --- lib/controllers/commit-detail-controller.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index b61cdf2177e..ef3a3f1e6c2 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -61,6 +61,11 @@ export default class CommitDetailController extends React.Component { return moment(date).fromNow(); } + getAuthorInfo() { + const coAuthorCount = this.props.commit.getCoAuthors().length; + return coAuthorCount ? this.props.commit.getAuthorEmail() : `${coAuthorCount + 1} people`; + } + renderAuthor(email) { const match = email.match(/^(\d+)\+[^@]+@users.noreply.github.com$/); From 200f0e790658dd55916861ba83ed2abc8f880aee Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 21 Nov 2018 16:42:01 -0800 Subject: [PATCH 009/117] add height so commit shows up --- lib/containers/commit-detail-container.js | 1 - lib/controllers/commit-detail-controller.js | 1 + lib/controllers/commit-preview-controller.js | 1 + lib/controllers/multi-file-patch-controller.js | 1 + lib/views/multi-file-patch-view.js | 3 ++- lib/views/pr-commit-view.js | 1 + 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/containers/commit-detail-container.js b/lib/containers/commit-detail-container.js index 1c5d3022d87..911c73b5f8f 100644 --- a/lib/containers/commit-detail-container.js +++ b/lib/containers/commit-detail-container.js @@ -19,7 +19,6 @@ export default class CommitDetailContainer extends React.Component { } render() { - console.log('container'); return ( {this.renderResult} diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index ef3a3f1e6c2..f9926d9ef49 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -51,6 +51,7 @@ export default class CommitDetailController extends React.Component {
diff --git a/lib/controllers/commit-preview-controller.js b/lib/controllers/commit-preview-controller.js index f1ce3c988c7..ff5f3cf72ba 100644 --- a/lib/controllers/commit-preview-controller.js +++ b/lib/controllers/commit-preview-controller.js @@ -23,6 +23,7 @@ export default class CommitPreviewController extends React.Component { return ( ); diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index cdba88def62..667184aae44 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -25,6 +25,7 @@ export default class MultiFilePatchController extends React.Component { discardLines: PropTypes.func, undoLastDiscard: PropTypes.func, surface: PropTypes.func, + autoHeight: PropTypes.bool, } constructor(props) { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 0c81bd6cfd3..7e2d7757998 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -58,6 +58,7 @@ export default class MultiFilePatchView extends React.Component { toggleSymlinkChange: PropTypes.func, undoLastDiscard: PropTypes.func, discardRows: PropTypes.func, + autoHeight: PropTypes.bool, refInitialFocus: RefHolderPropType, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, @@ -241,7 +242,7 @@ export default class MultiFilePatchView extends React.Component { buffer={this.props.multiFilePatch.getBuffer()} lineNumberGutterVisible={false} autoWidth={false} - autoHeight={false} + autoHeight={this.props.autoHeight} readOnly={true} softWrapped={true} diff --git a/lib/views/pr-commit-view.js b/lib/views/pr-commit-view.js index 88d879a3cbf..1d31e5947da 100644 --- a/lib/views/pr-commit-view.js +++ b/lib/views/pr-commit-view.js @@ -38,6 +38,7 @@ export class PrCommitView extends React.Component { } render() { + console.log('zzz'); const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; const {avatarUrl, name, date} = this.props.item.committer; return ( From 70bf46544bc37af6f19442bcf948aa6f374e8272 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 22 Nov 2018 10:13:16 +0900 Subject: [PATCH 010/117] Add scrolling --- styles/commit-detail.less | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 styles/commit-detail.less diff --git a/styles/commit-detail.less b/styles/commit-detail.less new file mode 100644 index 00000000000..03fc322e950 --- /dev/null +++ b/styles/commit-detail.less @@ -0,0 +1,10 @@ +@import "variables"; + +.github-CommitDetail { + + &-root { + // TODO: Remove if CommitDetailView gets moved to a editor decoration + overflow-y: auto; + } + +} From 8fbb4fc0651f9f87446f9a3b30312d30ea01451b Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 22 Nov 2018 11:03:46 +0900 Subject: [PATCH 011/117] Style CommitDetail header --- lib/controllers/commit-detail-controller.js | 44 ++++++----- styles/commit-detail.less | 87 +++++++++++++++++++++ 2 files changed, 111 insertions(+), 20 deletions(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index f9926d9ef49..7d229933340 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -27,28 +27,32 @@ export default class CommitDetailController extends React.Component { // const {avatarUrl, name, date} = this.props.item.committer; return ( -
-
-

- {emojify(commit.getMessageSubject())} -
-              {emojify(commit.getMessageBody())}
-

-
- {/* TODO fix image src */} - {this.renderAuthors()} - - {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} - +
+
+
+
+

+ {emojify(commit.getMessageSubject())} +

+
+                {emojify(commit.getMessageBody())}
+
+ {/* TODO fix image src */} + {this.renderAuthors()} + + {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} + +
+
+
+ {/* TODO fix href */} + + {commit.getSha()} + +
-
- {/* TODO fix href */} - - {commit.getSha()} - -
Date: Thu, 22 Nov 2018 22:01:51 +0100 Subject: [PATCH 012/117] Click on a recent commit to reveal `CommitDetailItem` --- lib/controllers/recent-commits-controller.js | 19 +++++++++++++++++++ lib/views/git-tab-view.js | 2 ++ lib/views/open-commit-dialog.js | 1 - lib/views/recent-commits-view.js | 7 ++++++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index 60ba927619b..7e3a8243096 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -1,6 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {autobind} from '../helpers'; +import {addEvent} from '../reporter-proxy'; +import CommitDetailItem from '../items/commit-detail-item'; import RecentCommitsView from '../views/recent-commits-view'; export default class RecentCommitsController extends React.Component { @@ -8,6 +11,13 @@ export default class RecentCommitsController extends React.Component { commits: PropTypes.arrayOf(PropTypes.object).isRequired, isLoading: PropTypes.bool.isRequired, undoLastCommit: PropTypes.func.isRequired, + workspace: PropTypes.object.isRequired, + repository: PropTypes.object.isRequired, + } + + constructor(props, context) { + super(props, context); + autobind(this, 'openCommit'); } render() { @@ -16,7 +26,16 @@ export default class RecentCommitsController extends React.Component { commits={this.props.commits} isLoading={this.props.isLoading} undoLastCommit={this.props.undoLastCommit} + openCommit={this.openCommit} /> ); } + + openCommit({sha}) { + const workdir = this.props.repository.getWorkingDirectoryPath(); + const uri = CommitDetailItem.buildURI(workdir, sha); + this.props.workspace.open(uri).then(() => { + addEvent('open-commit-in-pane', {package: 'github', from: 'dialog'}); + }); + } } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 9899daca097..fb7339869db 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -196,6 +196,8 @@ export default class GitTabView extends React.Component { commits={this.props.recentCommits} isLoading={this.props.isLoading} undoLastCommit={this.props.undoLastCommit} + workspace={this.props.workspace} + repository={this.props.repository} />
); diff --git a/lib/views/open-commit-dialog.js b/lib/views/open-commit-dialog.js index 6cd2f04d09e..47ab2d3ec21 100644 --- a/lib/views/open-commit-dialog.js +++ b/lib/views/open-commit-dialog.js @@ -22,7 +22,6 @@ export default class OpenCommitDialog extends React.Component { constructor(props, context) { super(props, context); autobind(this, 'accept', 'cancel', 'editorRefs', 'didChangeCommitSha'); - console.log('sdfasdf'); this.state = { cloneDisabled: false, diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 855fad6515a..5ca08ccffeb 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -11,6 +11,7 @@ class RecentCommitView extends React.Component { commit: PropTypes.object.isRequired, undoLastCommit: PropTypes.func.isRequired, isMostRecent: PropTypes.bool.isRequired, + openCommit: PropTypes.func.isRequired, }; render() { @@ -18,7 +19,9 @@ class RecentCommitView extends React.Component { const fullMessage = this.props.commit.getFullMessage(); return ( -
  • +
  • {this.renderAuthors()} this.props.openCommit({sha: commit.getSha()})} /> ); })} From 6914f2e21550e2a6ec0efb3c8041584b8145ab00 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 22 Nov 2018 22:36:45 +0100 Subject: [PATCH 013/117] telemetry event --- lib/controllers/recent-commits-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index 7e3a8243096..e29e9a2dac4 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -35,7 +35,7 @@ export default class RecentCommitsController extends React.Component { const workdir = this.props.repository.getWorkingDirectoryPath(); const uri = CommitDetailItem.buildURI(workdir, sha); this.props.workspace.open(uri).then(() => { - addEvent('open-commit-in-pane', {package: 'github', from: 'dialog'}); + addEvent('open-commit-in-pane', {package: 'github', from: 'recent commit'}); }); } } From 5bb615828e3e7b3b81a612d1f4caa4c46f8bbca5 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 22 Nov 2018 22:51:32 +0100 Subject: [PATCH 014/117] fix date --- lib/controllers/commit-detail-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index 7d229933340..4d9ecebcc44 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -63,7 +63,7 @@ export default class CommitDetailController extends React.Component { } humanizeTimeSince(date) { - return moment(date).fromNow(); + return moment(date * 1000).fromNow(); } getAuthorInfo() { From 4735badba64a201a6ee46692b5eef97b6293351c Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 23 Nov 2018 17:04:51 +0100 Subject: [PATCH 015/117] selected state for each recent commit --- lib/controllers/recent-commits-controller.js | 29 +++++++++++++++++++- lib/views/recent-commits-view.js | 8 +++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index e29e9a2dac4..e88ec50959b 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -4,7 +4,9 @@ import {autobind} from '../helpers'; import {addEvent} from '../reporter-proxy'; import CommitDetailItem from '../items/commit-detail-item'; +import URIPattern from '../atom/uri-pattern'; import RecentCommitsView from '../views/recent-commits-view'; +import {CompositeDisposable} from 'event-kit'; export default class RecentCommitsController extends React.Component { static propTypes = { @@ -17,7 +19,31 @@ export default class RecentCommitsController extends React.Component { constructor(props, context) { super(props, context); - autobind(this, 'openCommit'); + autobind(this, 'openCommit', 'updateSelectedCommit'); + + this.subscriptions = new CompositeDisposable( + this.props.workspace.onDidChangeActivePaneItem(this.updateSelectedCommit), + ); + this.state = {selectedCommitSha: ''}; + } + + updateSelectedCommit() { + const activeItem = this.props.workspace.getActivePaneItem(); + + const pattern = new URIPattern(decodeURIComponent( + CommitDetailItem.buildURI( + this.props.repository.getWorkingDirectoryPath(), + '{sha}'), + )); + + if (activeItem && activeItem.getURI) { + const match = pattern.matches(activeItem.getURI()); + if (match.ok()) { + const {sha} = match.getParams(); + return new Promise(resolve => this.setState({selectedCommitSha: sha}, resolve)); + } + } + return Promise.resolve(); } render() { @@ -27,6 +53,7 @@ export default class RecentCommitsController extends React.Component { isLoading={this.props.isLoading} undoLastCommit={this.props.undoLastCommit} openCommit={this.openCommit} + selectedCommitSha={this.state.selectedCommitSha} /> ); } diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 5ca08ccffeb..0f01a6f3f80 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -12,6 +12,7 @@ class RecentCommitView extends React.Component { undoLastCommit: PropTypes.func.isRequired, isMostRecent: PropTypes.bool.isRequired, openCommit: PropTypes.func.isRequired, + isSelected: PropTypes.bool.isRequired, }; render() { @@ -20,7 +21,10 @@ class RecentCommitView extends React.Component { return (
  • {this.renderAuthors()} this.props.openCommit({sha: commit.getSha()})} + isSelected={this.props.selectedCommitSha === commit.getSha()} /> ); })} From 140dc2577a9879282b4a39f358d21364d6467ec7 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 23 Nov 2018 17:05:04 +0100 Subject: [PATCH 016/117] visually show selected state --- styles/recent-commits.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/styles/recent-commits.less b/styles/recent-commits.less index 06a3bda3b56..31e0d2034c4 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -87,6 +87,12 @@ color: @text-color-subtle; } + &.is-selected { + // is selected + color: @text-color-selected; + background: @background-color-selected; + } + } From 8a76a617647aef7a6fbd1d0c1f01cefe23b9beb5 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 23 Nov 2018 18:11:21 +0100 Subject: [PATCH 017/117] match sha better --- lib/controllers/recent-commits-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index e88ec50959b..2e72da64f97 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -38,8 +38,8 @@ export default class RecentCommitsController extends React.Component { if (activeItem && activeItem.getURI) { const match = pattern.matches(activeItem.getURI()); - if (match.ok()) { - const {sha} = match.getParams(); + const {sha} = match.getParams(); + if (match.ok() && sha && sha !== this.state.selectedCommitSha) { return new Promise(resolve => this.setState({selectedCommitSha: sha}, resolve)); } } From 41eb1c39fa895733e5d38be22fa2c4375f9648cc Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 23 Nov 2018 18:11:52 +0100 Subject: [PATCH 018/117] stop propagation after undoing last commit --- lib/views/recent-commits-view.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 0f01a6f3f80..fd0bea625c7 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -35,7 +35,7 @@ class RecentCommitView extends React.Component { {this.props.isMostRecent && ( )} @@ -80,6 +80,11 @@ class RecentCommitView extends React.Component { ); } + + undoLastCommit = event => { + event.stopPropagation(); + this.props.undoLastCommit(); + } } export default class RecentCommitsView extends React.Component { From c9c1c07309b837e071a9766e5f819c2809528567 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 23 Nov 2018 18:45:31 +0100 Subject: [PATCH 019/117] disable most of the file patch header functionality when it's in a commit detail item --- lib/controllers/commit-detail-controller.js | 1 + lib/controllers/multi-file-patch-controller.js | 1 + lib/views/file-patch-header-view.js | 7 ++++--- lib/views/multi-file-patch-view.js | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index 4d9ecebcc44..ab5ca279533 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -57,6 +57,7 @@ export default class CommitDetailController extends React.Component { multiFilePatch={commit.getMultiFileDiff()} autoHeight={true} {...this.props} + disableStageUnstage={true} />
  • ); diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 667184aae44..139d15a0a34 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -26,6 +26,7 @@ export default class MultiFilePatchController extends React.Component { undoLastDiscard: PropTypes.func, surface: PropTypes.func, autoHeight: PropTypes.bool, + disableStageUnstage: PropTypes.bool, } constructor(props) { diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 97a54c81d9a..c76311f3530 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -26,6 +26,7 @@ export default class FilePatchHeaderView extends React.Component { toggleFile: PropTypes.func.isRequired, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, + disableStageUnstage: PropTypes.bool, }; constructor(props) { @@ -75,10 +76,10 @@ export default class FilePatchHeaderView extends React.Component { renderButtonGroup() { return ( - {this.renderUndoDiscardButton()} - {this.renderMirrorPatchButton()} + {!this.props.disableStageUnstage && this.renderUndoDiscardButton()} + {!this.props.disableStageUnstage && this.renderMirrorPatchButton()} {this.renderOpenFileButton()} - {this.renderToggleFileButton()} + {!this.props.disableStageUnstage && this.renderToggleFileButton()} ); } diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 7e2d7757998..a066c29bac4 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -59,7 +59,7 @@ export default class MultiFilePatchView extends React.Component { undoLastDiscard: PropTypes.func, discardRows: PropTypes.func, autoHeight: PropTypes.bool, - + disableStageUnstage: PropTypes.bool, refInitialFocus: RefHolderPropType, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, } @@ -329,6 +329,7 @@ export default class MultiFilePatchView extends React.Component { diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} openFile={() => this.didOpenFile({selectedFilePatch: filePatch})} toggleFile={() => this.props.toggleFile(filePatch)} + disableStageUnstage={this.props.disableStageUnstage} /> {this.renderSymlinkChangeMeta(filePatch)} {this.renderExecutableModeChangeMeta(filePatch)} @@ -504,6 +505,8 @@ export default class MultiFilePatchView extends React.Component { toggleSelection={() => this.toggleHunkSelection(hunk, containsSelection)} discardSelection={() => this.discardHunkSelection(hunk, containsSelection)} mouseDown={this.didMouseDownOnHeader} + + disableStageUnstage={this.props.disableStageUnstage} /> From b80ee27e0f145a2be86c41641a6673836732aeb2 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 23 Nov 2018 18:45:44 +0100 Subject: [PATCH 020/117] same for hunk header --- lib/views/hunk-header-view.js | 59 +++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index 8ee5e96ddad..0c4dd8516d9 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -28,11 +28,12 @@ export default class HunkHeaderView extends React.Component { toggleSelection: PropTypes.func, discardSelection: PropTypes.func, mouseDown: PropTypes.func.isRequired, + disableStageUnstage: PropTypes.bool, }; constructor(props) { super(props); - autobind(this, 'didMouseDown'); + autobind(this, 'didMouseDown', 'renderButtons'); this.refDiscardButton = new RefHolder(); } @@ -48,32 +49,44 @@ export default class HunkHeaderView extends React.Component { {this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()} - - {this.props.stagingStatus === 'unstaged' && ( - -
    ); } + renderButtons() { + if (this.props.disableStageUnstage) { + return null; + } else { + return ( + + + {this.props.stagingStatus === 'unstaged' && ( + +
    diff --git a/styles/commit-detail.less b/styles/commit-detail.less index fc8db7a9404..6f8c24f975a 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -3,19 +3,13 @@ @default-padding: @component-padding; @avatar-dimensions: 16px; -.github-CommitDetail { - - &-root { - // TODO: Remove if CommitDetailView gets moved to a editor decoration - overflow-y: auto; - } - -} - - .github-CommitDetailView { + display: flex; + flex-direction: column; + height: 100%; &-header { + flex: 0; padding: @default-padding; padding-bottom: 0; background-color: @syntax-background-color; From 5c7aa08f03effb162b2408255cd0d2e08c9fda5c Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 28 Nov 2018 14:51:58 +0100 Subject: [PATCH 036/117] remove jump to file --- lib/views/file-patch-header-view.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index c76311f3530..5eb5476d08a 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -74,14 +74,18 @@ export default class FilePatchHeaderView extends React.Component { } renderButtonGroup() { - return ( - - {!this.props.disableStageUnstage && this.renderUndoDiscardButton()} - {!this.props.disableStageUnstage && this.renderMirrorPatchButton()} - {this.renderOpenFileButton()} - {!this.props.disableStageUnstage && this.renderToggleFileButton()} - - ); + if (this.props.disableStageUnstage) { + return null; + } else { + return ( + + {this.renderUndoDiscardButton()} + {this.renderMirrorPatchButton()} + {this.renderOpenFileButton()} + {this.renderToggleFileButton()} + + ); + } } renderUndoDiscardButton() { From fc6ca8ac95dc2d25627383a4347cbe788bfdea2e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 10:38:09 -0500 Subject: [PATCH 037/117] Cover that last line in CommitDetailItem --- test/items/commit-detail-item.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/items/commit-detail-item.test.js b/test/items/commit-detail-item.test.js index 7199b6b20d5..6a6d4e89d3e 100644 --- a/test/items/commit-detail-item.test.js +++ b/test/items/commit-detail-item.test.js @@ -152,4 +152,17 @@ describe.only('CommitDetailItem', function() { assert.strictEqual(item.getSha(), '420'); }); + it('passes a focus() call to the component designated as its initial focus', async function() { + const wrapper = mount(buildPaneApp()); + const item = await open(wrapper); + wrapper.update(); + + const refHolder = wrapper.find('CommitDetailContainer').prop('refInitialFocus'); + const initialFocus = await refHolder.getPromise(); + sinon.spy(initialFocus, 'focus'); + + item.focus(); + + assert.isTrue(initialFocus.focus.called); + }); }); From b61b9ae96f5c4ee89941f2f86730fece51d88ea2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 10:38:16 -0500 Subject: [PATCH 038/117] :fire: dot only --- test/items/commit-detail-item.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/items/commit-detail-item.test.js b/test/items/commit-detail-item.test.js index 6a6d4e89d3e..eecbc66cf6a 100644 --- a/test/items/commit-detail-item.test.js +++ b/test/items/commit-detail-item.test.js @@ -6,7 +6,7 @@ import PaneItem from '../../lib/atom/pane-item'; import WorkdirContextPool from '../../lib/models/workdir-context-pool'; import {cloneRepository} from '../helpers'; -describe.only('CommitDetailItem', function() { +describe('CommitDetailItem', function() { let atomEnv, repository, pool; beforeEach(async function() { From 867397f5b48266a486fb3a509762377a7ed8c7f3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 10:46:13 -0500 Subject: [PATCH 039/117] Ensure we return a valid Commit to suppress console errors --- test/containers/commit-detail-container.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/containers/commit-detail-container.test.js b/test/containers/commit-detail-container.test.js index 2fccdc3614f..e4dff8718fc 100644 --- a/test/containers/commit-detail-container.test.js +++ b/test/containers/commit-detail-container.test.js @@ -5,6 +5,8 @@ import CommitDetailContainer from '../../lib/containers/commit-detail-container' import CommitDetailItem from '../../lib/items/commit-detail-item'; import {cloneRepository, buildRepository} from '../helpers'; +const VALID_SHA = '18920c900bfa6e4844853e7e246607a31c3e2e8c'; + describe('CommitDetailContainer', function() { let atomEnv, repository; @@ -23,7 +25,7 @@ describe('CommitDetailContainer', function() { const props = { repository, - sha: '18920c900bfa6e4844853e7e246607a31c3e2e8c', + sha: VALID_SHA, itemType: CommitDetailItem, workspace: atomEnv.workspace, @@ -47,7 +49,7 @@ describe('CommitDetailContainer', function() { it('renders a loading spinner while the file patch is being loaded', async function() { await repository.getLoadPromise(); - const patchPromise = repository.getStagedChangesPatch(); + const commitPromise = repository.getCommit(VALID_SHA); let resolveDelayedPromise = () => {}; const delayedPromise = new Promise(resolve => { resolveDelayedPromise = resolve; @@ -57,13 +59,13 @@ describe('CommitDetailContainer', function() { const wrapper = mount(buildApp()); assert.isTrue(wrapper.find('LoadingView').exists()); - resolveDelayedPromise(patchPromise); + resolveDelayedPromise(commitPromise); await assert.async.isFalse(wrapper.update().find('LoadingView').exists()); }); it('renders a CommitDetailController once the commit is loaded', async function() { await repository.getLoadPromise(); - const commit = await repository.getCommit('18920c900bfa6e4844853e7e246607a31c3e2e8c'); + const commit = await repository.getCommit(VALID_SHA); const wrapper = mount(buildApp()); await assert.async.isTrue(wrapper.update().find('CommitDetailController').exists()); From b27744610eab776f72814bbc14fe7bc480fcbe1e Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 28 Nov 2018 18:50:04 +0100 Subject: [PATCH 040/117] remove `disableStageUnstage` flag and use `itemType` as checker instead --- lib/controllers/commit-detail-controller.js | 1 - lib/controllers/multi-file-patch-controller.js | 1 - lib/views/file-patch-header-view.js | 3 +-- lib/views/hunk-header-view.js | 1 - lib/views/multi-file-patch-view.js | 4 ---- test/controllers/commit-detail-controller.test.js | 3 +-- 6 files changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index d73bf852f23..b5b3c3f5bbe 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -57,7 +57,6 @@ export default class CommitDetailController extends React.Component { multiFilePatch={commit.getMultiFileDiff()} autoHeight={false} {...this.props} - disableStageUnstage={true} /> ); diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 139d15a0a34..667184aae44 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -26,7 +26,6 @@ export default class MultiFilePatchController extends React.Component { undoLastDiscard: PropTypes.func, surface: PropTypes.func, autoHeight: PropTypes.bool, - disableStageUnstage: PropTypes.bool, } constructor(props) { diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 5eb5476d08a..51131689f0f 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -26,7 +26,6 @@ export default class FilePatchHeaderView extends React.Component { toggleFile: PropTypes.func.isRequired, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, - disableStageUnstage: PropTypes.bool, }; constructor(props) { @@ -74,7 +73,7 @@ export default class FilePatchHeaderView extends React.Component { } renderButtonGroup() { - if (this.props.disableStageUnstage) { + if (this.props.itemType === CommitDetailItem) { return null; } else { return ( diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index 0c4dd8516d9..57e71a28a31 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -28,7 +28,6 @@ export default class HunkHeaderView extends React.Component { toggleSelection: PropTypes.func, discardSelection: PropTypes.func, mouseDown: PropTypes.func.isRequired, - disableStageUnstage: PropTypes.bool, }; constructor(props) { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index a066c29bac4..3cb5a617314 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -59,7 +59,6 @@ export default class MultiFilePatchView extends React.Component { undoLastDiscard: PropTypes.func, discardRows: PropTypes.func, autoHeight: PropTypes.bool, - disableStageUnstage: PropTypes.bool, refInitialFocus: RefHolderPropType, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, } @@ -329,7 +328,6 @@ export default class MultiFilePatchView extends React.Component { diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} openFile={() => this.didOpenFile({selectedFilePatch: filePatch})} toggleFile={() => this.props.toggleFile(filePatch)} - disableStageUnstage={this.props.disableStageUnstage} /> {this.renderSymlinkChangeMeta(filePatch)} {this.renderExecutableModeChangeMeta(filePatch)} @@ -505,8 +503,6 @@ export default class MultiFilePatchView extends React.Component { toggleSelection={() => this.toggleHunkSelection(hunk, containsSelection)} discardSelection={() => this.discardHunkSelection(hunk, containsSelection)} mouseDown={this.didMouseDownOnHeader} - - disableStageUnstage={this.props.disableStageUnstage} /> diff --git a/test/controllers/commit-detail-controller.test.js b/test/controllers/commit-detail-controller.test.js index d045d8b6d5f..92a5659a22a 100644 --- a/test/controllers/commit-detail-controller.test.js +++ b/test/controllers/commit-detail-controller.test.js @@ -41,10 +41,9 @@ describe('CommitDetailController', function() { return ; } - it('has a MultiFilePatchController that has `disableStageUnstage` flag set to true', function() { + it('has a MultiFilePatchController', function() { const wrapper = mount(buildApp()); assert.isTrue(wrapper.find('MultiFilePatchController').exists()); - assert.isTrue(wrapper.find('MultiFilePatchController').prop('disableStageUnstage')); }); it('passes unrecognized props to a MultiFilePatchController', function() { From e8620da653cefc3b17e988e11175d927ee667aa1 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 28 Nov 2018 18:50:35 +0100 Subject: [PATCH 041/117] use `itemType` in hunk headers too! --- lib/views/hunk-header-view.js | 6 +++++- lib/views/multi-file-patch-view.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index 57e71a28a31..6fd986ee542 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -7,6 +7,9 @@ import {RefHolderPropType} from '../prop-types'; import RefHolder from '../models/ref-holder'; import Tooltip from '../atom/tooltip'; import Keystroke from '../atom/keystroke'; +import ChangedFileItem from '../items/changed-file-item'; +import CommitPreviewItem from '../items/commit-preview-item'; +import CommitDetailItem from '../items/commit-detail-item'; function theBuckStopsHere(event) { event.stopPropagation(); @@ -28,6 +31,7 @@ export default class HunkHeaderView extends React.Component { toggleSelection: PropTypes.func, discardSelection: PropTypes.func, mouseDown: PropTypes.func.isRequired, + itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, }; constructor(props) { @@ -54,7 +58,7 @@ export default class HunkHeaderView extends React.Component { } renderButtons() { - if (this.props.disableStageUnstage) { + if (this.props.itemType === CommitPreviewItem) { return null; } else { return ( diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 3cb5a617314..5e3d3d73bfc 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -503,6 +503,7 @@ export default class MultiFilePatchView extends React.Component { toggleSelection={() => this.toggleHunkSelection(hunk, containsSelection)} discardSelection={() => this.discardHunkSelection(hunk, containsSelection)} mouseDown={this.didMouseDownOnHeader} + itemType={this.props.itemType} /> From 22718c24c38b98b0b4cfbe7e9e9020373a0a0eac Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 13:48:21 -0500 Subject: [PATCH 042/117] Totally arbitrary "long commit message threshold" --- lib/models/commit.js | 10 ++++++++ test/models/commit.test.js | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 test/models/commit.test.js diff --git a/lib/models/commit.js b/lib/models/commit.js index f1fbd72a1c2..c568de80665 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -1,6 +1,8 @@ const UNBORN = Symbol('unborn'); export default class Commit { + static LONG_MESSAGE_THRESHOLD = 1000; + static createUnborn() { return new Commit({unbornRef: UNBORN}); } @@ -40,6 +42,10 @@ export default class Commit { return this.messageBody; } + isBodyLong() { + return this.getMessageBody().length > this.constructor.LONG_MESSAGE_THRESHOLD; + } + getFullMessage() { return `${this.getMessageSubject()}\n\n${this.getMessageBody()}`.trim(); } @@ -77,4 +83,8 @@ export const nullCommit = { isPresent() { return false; }, + + isBodyLong() { + return false; + }, }; diff --git a/test/models/commit.test.js b/test/models/commit.test.js new file mode 100644 index 00000000000..a8cd468a4c3 --- /dev/null +++ b/test/models/commit.test.js @@ -0,0 +1,51 @@ +import moment from 'moment'; +import dedent from 'dedent-js'; + +import Commit, {nullCommit} from '../../lib/models/commit'; + +describe('Commit', function() { + function buildCommit(override = {}) { + return new Commit({ + sha: '0123456789abcdefghij0123456789abcdefghij', + authorEmail: 'me@email.com', + coAuthors: [], + authorDate: moment('2018-11-28T12:00:00', moment.ISO_8601).unix(), + messageSubject: 'subject', + messageBody: 'body', + ...override, + }); + } + + describe('isBodyLong()', function() { + it('returns false if the commit message body is short', function() { + assert.isFalse(buildCommit({messageBody: 'short'}).isBodyLong()); + }); + + it('returns true if the commit message body is long', function() { + const messageBody = dedent` + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, + essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. + + Ea salutatus contentiones eos. Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere + urbanitas, usu ut aperiri mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, + scripta iudicabit ne nam, in duis clita commodo sit. + + Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et eum + voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus + tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam + reprehendunt et mea. Ea eius omnes voluptua sit. + + No cum illud verear efficiantur. Id altera imperdiet nec. Noster audiam accusamus mei at, no zril libris nemore + duo, ius ne rebum doctus fuisset. Legimus epicurei in sit, esse purto suscipit eu qui, oporteat deserunt + delicatissimi sea in. Est id putent accusata convenire, no tibique molestie accommodare quo, cu est fuisset + offendit evertitur. + `; + assert.isTrue(buildCommit({messageBody}).isBodyLong()); + }); + + it('returns false for a null commit', function() { + assert.isFalse(nullCommit.isBodyLong()); + }); + }); +}); From 8434c08aec30d92726d4ac2599297d1e132ddf19 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 13:59:03 -0500 Subject: [PATCH 043/117] CommitDetailController is only responsible for message collapse state All markup generation has been moved (verbatim) to CommitDetailView. --- lib/controllers/commit-detail-controller.js | 102 +++------------- lib/views/commit-detail-view.js | 102 ++++++++++++++++ .../commit-detail-controller.test.js | 110 ++++++++++-------- test/views/commit-detail-view.test.js | 69 +++++++++++ 4 files changed, 248 insertions(+), 135 deletions(-) create mode 100644 lib/views/commit-detail-view.js create mode 100644 test/views/commit-detail-view.test.js diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index b5b3c3f5bbe..6e0df0c146c 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -1,104 +1,38 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {emojify} from 'node-emoji'; -import moment from 'moment'; -import MultiFilePatchController from './multi-file-patch-controller'; - -const avatarAltText = 'committer avatar'; +import CommitDetailView from '../views/commit-detail-view'; export default class CommitDetailController extends React.Component { static propTypes = { - repository: PropTypes.object.isRequired, - - workspace: PropTypes.object.isRequired, - commands: PropTypes.object.isRequired, - keymaps: PropTypes.object.isRequired, - tooltips: PropTypes.object.isRequired, - config: PropTypes.object.isRequired, + ...CommitDetailView.propTypes, - destroy: PropTypes.func.isRequired, commit: PropTypes.object.isRequired, } - render() { - const commit = this.props.commit; - // const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; - // const {avatarUrl, name, date} = this.props.item.committer; - - return ( -
    -
    -
    -
    -

    - {emojify(commit.getMessageSubject())} -

    -
    -                {emojify(commit.getMessageBody())}
    -
    - {/* TODO fix image src */} - {this.renderAuthors()} - - {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} - -
    -
    -
    - {/* TODO fix href */} - - {commit.getSha()} - -
    -
    -
    - -
    - ); - } + constructor(props) { + super(props); - humanizeTimeSince(date) { - return moment(date * 1000).fromNow(); + this.state = { + messageCollapsible: this.props.commit.isBodyLong(), + messageOpen: !this.props.commit.isBodyLong(), + }; } - getAuthorInfo() { - const coAuthorCount = this.props.commit.getCoAuthors().length; - return coAuthorCount ? this.props.commit.getAuthorEmail() : `${coAuthorCount + 1} people`; - } - - renderAuthor(email) { - const match = email.match(/^(\d+)\+[^@]+@users.noreply.github.com$/); - - let avatarUrl; - if (match) { - avatarUrl = 'https://avatars.githubusercontent.com/u/' + match[1] + '?s=32'; - } else { - avatarUrl = 'https://avatars.githubusercontent.com/u/e?email=' + encodeURIComponent(email) + '&s=32'; - } - + render() { return ( - {`${email}'s ); } - renderAuthors() { - const coAuthorEmails = this.props.commit.getCoAuthors().map(author => author.email); - const authorEmails = [this.props.commit.getAuthorEmail(), ...coAuthorEmails]; - - return ( - - {authorEmails.map(this.renderAuthor)} - - ); + toggleMessage = () => { + return new Promise(resolve => { + this.setState(prevState => ({messageOpen: !prevState.messageOpen}), resolve); + }); } } diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js new file mode 100644 index 00000000000..96b38f1ec61 --- /dev/null +++ b/lib/views/commit-detail-view.js @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {emojify} from 'node-emoji'; +import moment from 'moment'; + +import MultiFilePatchController from '../controllers/multi-file-patch-controller'; + +export default class CommitDetailView extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + + workspace: PropTypes.object.isRequired, + commands: PropTypes.object.isRequired, + keymaps: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + + destroy: PropTypes.func.isRequired, + commit: PropTypes.object.isRequired, + } + + render() { + const commit = this.props.commit; + // const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; + // const {avatarUrl, name, date} = this.props.item.committer; + + return ( +
    +
    +
    +
    +

    + {emojify(commit.getMessageSubject())} +

    +
    +                {emojify(commit.getMessageBody())}
    +
    + {/* TODO fix image src */} + {this.renderAuthors()} + + {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} + +
    +
    +
    + {/* TODO fix href */} + + {commit.getSha()} + +
    +
    +
    + +
    + ); + } + + humanizeTimeSince(date) { + return moment(date * 1000).fromNow(); + } + + getAuthorInfo() { + const coAuthorCount = this.props.commit.getCoAuthors().length; + return coAuthorCount ? this.props.commit.getAuthorEmail() : `${coAuthorCount + 1} people`; + } + + renderAuthor(email) { + const match = email.match(/^(\d+)\+[^@]+@users.noreply.github.com$/); + + let avatarUrl; + if (match) { + avatarUrl = 'https://avatars.githubusercontent.com/u/' + match[1] + '?s=32'; + } else { + avatarUrl = 'https://avatars.githubusercontent.com/u/e?email=' + encodeURIComponent(email) + '&s=32'; + } + + return ( + {`${email}'s + ); + } + + renderAuthors() { + const coAuthorEmails = this.props.commit.getCoAuthors().map(author => author.email); + const authorEmails = [this.props.commit.getAuthorEmail(), ...coAuthorEmails]; + + return ( + + {authorEmails.map(this.renderAuthor)} + + ); + } +} diff --git a/test/controllers/commit-detail-controller.test.js b/test/controllers/commit-detail-controller.test.js index 92a5659a22a..2231af47da2 100644 --- a/test/controllers/commit-detail-controller.test.js +++ b/test/controllers/commit-detail-controller.test.js @@ -1,21 +1,20 @@ import React from 'react'; -import moment from 'moment'; -import {shallow, mount} from 'enzyme'; +import {shallow} from 'enzyme'; +import dedent from 'dedent-js'; import {cloneRepository, buildRepository} from '../helpers'; import CommitDetailItem from '../../lib/items/commit-detail-item'; import CommitDetailController from '../../lib/controllers/commit-detail-controller'; -import Commit from '../../lib/models/commit'; -import {multiFilePatchBuilder} from '../builder/patch'; -describe('CommitDetailController', function() { +const VALID_SHA = '18920c900bfa6e4844853e7e246607a31c3e2e8c'; +describe('CommitDetailController', function() { let atomEnv, repository, commit; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); repository = await buildRepository(await cloneRepository('multiple-commits')); - commit = await repository.getCommit('18920c900bfa6e4844853e7e246607a31c3e2e8c'); + commit = await repository.getCommit(VALID_SHA); }); afterEach(function() { @@ -41,59 +40,68 @@ describe('CommitDetailController', function() { return ; } - it('has a MultiFilePatchController', function() { - const wrapper = mount(buildApp()); - assert.isTrue(wrapper.find('MultiFilePatchController').exists()); + it('forwards props to its CommitDetailView', function() { + const wrapper = shallow(buildApp()); + const view = wrapper.find('CommitDetailView'); + + assert.strictEqual(view.prop('repository'), repository); + assert.strictEqual(view.prop('commit'), commit); + assert.strictEqual(view.prop('itemType'), CommitDetailItem); }); - it('passes unrecognized props to a MultiFilePatchController', function() { + it('passes unrecognized props to its CommitDetailView', function() { const extra = Symbol('extra'); const wrapper = shallow(buildApp({extra})); - - assert.strictEqual(wrapper.find('MultiFilePatchController').prop('extra'), extra); + assert.strictEqual(wrapper.find('CommitDetailView').prop('extra'), extra); }); - it('renders commit details properly', function() { - const newCommit = new Commit({ - sha: '420', - authorEmail: 'very@nice.com', - authorDate: moment().subtract(2, 'days').unix(), - messageSubject: 'subject', - messageBody: 'messageBody', + describe('commit body collapsing', function() { + const LONG_MESSAGE = dedent` + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, + essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. + + Ea salutatus contentiones eos. Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere + urbanitas, usu ut aperiri mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, scripta + iudicabit ne nam, in duis clita commodo sit. + + Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et eum + voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus tractatos + ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam reprehendunt et + mea. Ea eius omnes voluptua sit. + + No cum illud verear efficiantur. Id altera imperdiet nec. Noster audiam accusamus mei at, no zril libris nemore + duo, ius ne rebum doctus fuisset. Legimus epicurei in sit, esse purto suscipit eu qui, oporteat deserunt + delicatissimi sea in. Est id putent accusata convenire, no tibique molestie accommodare quo, cu est fuisset + offendit evertitur. + `; + + it('is uncollapsible if the commit message is short', function() { + sinon.stub(commit, 'getMessageBody').returns('short'); + const wrapper = shallow(buildApp()); + const view = wrapper.find('CommitDetailView'); + assert.isFalse(view.prop('messageCollapsible')); + assert.isTrue(view.prop('messageOpen')); }); - const {multiFilePatch: mfp} = multiFilePatchBuilder().addFilePatch().build(); - sinon.stub(newCommit, 'getMultiFileDiff').returns(mfp); - const wrapper = mount(buildApp({commit: newCommit})); - - assert.strictEqual(wrapper.find('.github-CommitDetailView-title').text(), 'subject'); - assert.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), 'messageBody'); - assert.strictEqual(wrapper.find('.github-CommitDetailView-metaText').text(), 'very@nice.com committed 2 days ago'); - assert.strictEqual(wrapper.find('.github-CommitDetailView-sha').text(), '420'); - /* TODO fix href test */ - // assert.strictEqual(wrapper.find('.github-CommitDetailView-sha a').prop('href'), '420'); - assert.strictEqual(wrapper.find('img.github-RecentCommit-avatar').prop('src'), 'https://avatars.githubusercontent.com/u/e?email=very%40nice.com&s=32'); - }); - it('renders multiple avatars for co-authored commit', function() { - const newCommit = new Commit({ - sha: '420', - authorEmail: 'very@nice.com', - authorDate: moment().subtract(2, 'days').unix(), - messageSubject: 'subject', - messageBody: 'messageBody', - coAuthors: [{name: 'two', email: 'two@coauthor.com'}, {name: 'three', email: 'three@coauthor.com'}], + it('is collapsible and begins collapsed if the commit message is long', function() { + sinon.stub(commit, 'getMessageBody').returns(LONG_MESSAGE); + + const wrapper = shallow(buildApp()); + const view = wrapper.find('CommitDetailView'); + assert.isTrue(view.prop('messageCollapsible')); + assert.isFalse(view.prop('messageOpen')); }); - const {multiFilePatch: mfp} = multiFilePatchBuilder().addFilePatch().build(); - sinon.stub(newCommit, 'getMultiFileDiff').returns(mfp); - const wrapper = mount(buildApp({commit: newCommit})); - assert.deepEqual( - wrapper.find('img.github-RecentCommit-avatar').map(w => w.prop('src')), - [ - 'https://avatars.githubusercontent.com/u/e?email=very%40nice.com&s=32', - 'https://avatars.githubusercontent.com/u/e?email=two%40coauthor.com&s=32', - 'https://avatars.githubusercontent.com/u/e?email=three%40coauthor.com&s=32', - ], - ); - }); + it('toggles collapsed state', async function() { + sinon.stub(commit, 'getMessageBody').returns(LONG_MESSAGE); + + const wrapper = shallow(buildApp()); + assert.isFalse(wrapper.find('CommitDetailView').prop('messageOpen')); + + await wrapper.find('CommitDetailView').prop('toggleMessage')(); + + assert.isTrue(wrapper.find('CommitDetailView').prop('messageOpen')); + }); + }); }); diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js new file mode 100644 index 00000000000..731a5b71187 --- /dev/null +++ b/test/views/commit-detail-view.test.js @@ -0,0 +1,69 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +describe('CommitDetailView', function() { + it('has a MultiFilePatchController that its itemType set'); + + it('passes unrecognized props to a MultiFilePatchController'); + + it('renders commit details properly'); + + it('renders multiple avatars for co-authored commit'); +}); + +/* +it('has a MultiFilePatchController that has `disableStageUnstage` flag set to true', function() { + const wrapper = mount(buildApp()); + assert.isTrue(wrapper.find('MultiFilePatchController').exists()); + assert.isTrue(wrapper.find('MultiFilePatchController').prop('disableStageUnstage')); +}); + +it('passes unrecognized props to a MultiFilePatchController', function() { + const extra = Symbol('extra'); + const wrapper = shallow(buildApp({extra})); + + assert.strictEqual(wrapper.find('MultiFilePatchController').prop('extra'), extra); +}); + +it('renders commit details properly', function() { + const newCommit = new Commit({ + sha: '420', + authorEmail: 'very@nice.com', + authorDate: moment().subtract(2, 'days').unix(), + messageSubject: 'subject', + messageBody: 'messageBody', + }); + const {multiFilePatch: mfp} = multiFilePatchBuilder().addFilePatch().build(); + sinon.stub(newCommit, 'getMultiFileDiff').returns(mfp); + const wrapper = mount(buildApp({commit: newCommit})); + + assert.strictEqual(wrapper.find('.github-CommitDetailView-title').text(), 'subject'); + assert.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), 'messageBody'); + assert.strictEqual(wrapper.find('.github-CommitDetailView-metaText').text(), 'very@nice.com committed 2 days ago'); + assert.strictEqual(wrapper.find('.github-CommitDetailView-sha').text(), '420'); + // assert.strictEqual(wrapper.find('.github-CommitDetailView-sha a').prop('href'), '420'); + assert.strictEqual(wrapper.find('img.github-RecentCommit-avatar').prop('src'), 'https://avatars.githubusercontent.com/u/e?email=very%40nice.com&s=32'); +}); + +it('renders multiple avatars for co-authored commit', function() { + const newCommit = new Commit({ + sha: '420', + authorEmail: 'very@nice.com', + authorDate: moment().subtract(2, 'days').unix(), + messageSubject: 'subject', + messageBody: 'messageBody', + coAuthors: [{name: 'two', email: 'two@coauthor.com'}, {name: 'three', email: 'three@coauthor.com'}], + }); + const {multiFilePatch: mfp} = multiFilePatchBuilder().addFilePatch().build(); + sinon.stub(newCommit, 'getMultiFileDiff').returns(mfp); + const wrapper = mount(buildApp({commit: newCommit})); + assert.deepEqual( + wrapper.find('img.github-RecentCommit-avatar').map(w => w.prop('src')), + [ + 'https://avatars.githubusercontent.com/u/e?email=very%40nice.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=two%40coauthor.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=three%40coauthor.com&s=32', + ], + ); +}); +*/ From 23ed35b724d7b460acc2cde7d6d3e40d2fd13ec2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 14:36:30 -0500 Subject: [PATCH 044/117] Builder for creating test Commits --- test/builder/commit.js | 54 ++++++++++++++++++++++++++++++++++++++ test/models/commit.test.js | 22 +++++----------- 2 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 test/builder/commit.js diff --git a/test/builder/commit.js b/test/builder/commit.js new file mode 100644 index 00000000000..3d4dfef2061 --- /dev/null +++ b/test/builder/commit.js @@ -0,0 +1,54 @@ +import moment from 'moment'; + +import Commit from '../../lib/models/commit'; + +class CommitBuilder { + constructor() { + this._sha = '0123456789abcdefghij0123456789abcdefghij'; + this._authorEmail = 'default@email.com'; + this._authorDate = moment('2018-11-28T12:00:00', moment.ISO_8601).unix(); + this._coAuthors = []; + this._messageSubject = 'subject'; + this._messageBody = 'body'; + } + + sha(newSha) { + this._sha = newSha; + return this; + } + + authorEmail(newEmail) { + this._authorEmail = newEmail; + return this; + } + + authorDate(timestamp) { + this._authorDate = timestamp; + return this; + } + + messageSubject(subject) { + this._messageSubject = subject; + return this; + } + + messageBody(body) { + this._messageBody = body; + return this; + } + + build() { + return new Commit({ + sha: this._sha, + authorEmail: this._authorEmail, + authorDate: this._authorDate, + coAuthors: this._coAuthors, + messageSubject: this._messageSubject, + messageBody: this._messageBody, + }); + } +} + +export function commitBuilder() { + return new CommitBuilder(); +} diff --git a/test/models/commit.test.js b/test/models/commit.test.js index a8cd468a4c3..1adb497f100 100644 --- a/test/models/commit.test.js +++ b/test/models/commit.test.js @@ -1,24 +1,13 @@ -import moment from 'moment'; import dedent from 'dedent-js'; -import Commit, {nullCommit} from '../../lib/models/commit'; +import {nullCommit} from '../../lib/models/commit'; +import {commitBuilder} from '../builder/commit'; describe('Commit', function() { - function buildCommit(override = {}) { - return new Commit({ - sha: '0123456789abcdefghij0123456789abcdefghij', - authorEmail: 'me@email.com', - coAuthors: [], - authorDate: moment('2018-11-28T12:00:00', moment.ISO_8601).unix(), - messageSubject: 'subject', - messageBody: 'body', - ...override, - }); - } - describe('isBodyLong()', function() { it('returns false if the commit message body is short', function() { - assert.isFalse(buildCommit({messageBody: 'short'}).isBodyLong()); + const commit = commitBuilder().messageBody('short').build(); + assert.isFalse(commit.isBodyLong()); }); it('returns true if the commit message body is long', function() { @@ -41,7 +30,8 @@ describe('Commit', function() { delicatissimi sea in. Est id putent accusata convenire, no tibique molestie accommodare quo, cu est fuisset offendit evertitur. `; - assert.isTrue(buildCommit({messageBody}).isBodyLong()); + const commit = commitBuilder().messageBody(messageBody).build(); + assert.isTrue(commit.isBodyLong()); }); it('returns false for a null commit', function() { From d1237527de6eb258cb2bb0710d9807fb176b340b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 14:56:15 -0500 Subject: [PATCH 045/117] setMultiFileDiff() to construct a Commit's diff --- test/builder/commit.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/builder/commit.js b/test/builder/commit.js index 3d4dfef2061..a68ada437b6 100644 --- a/test/builder/commit.js +++ b/test/builder/commit.js @@ -1,6 +1,7 @@ import moment from 'moment'; import Commit from '../../lib/models/commit'; +import {multiFilePatchBuilder} from './patch'; class CommitBuilder { constructor() { @@ -10,6 +11,8 @@ class CommitBuilder { this._coAuthors = []; this._messageSubject = 'subject'; this._messageBody = 'body'; + + this._multiFileDiff = null; } sha(newSha) { @@ -37,8 +40,14 @@ class CommitBuilder { return this; } + setMultiFileDiff(block = () => {}) { + const builder = multiFilePatchBuilder(); + block(builder); + this._multiFileDiff = builder.build().multiFilePatch; + return this; + } build() { - return new Commit({ + const commit = new Commit({ sha: this._sha, authorEmail: this._authorEmail, authorDate: this._authorDate, @@ -46,6 +55,12 @@ class CommitBuilder { messageSubject: this._messageSubject, messageBody: this._messageBody, }); + + if (this._multiFileDiff !== null) { + commit.setMultiFileDiff(this._multiFileDiff); + } + + return commit; } } From a22b69233e5e10e6fc8724af2ee52cdeed8379ed Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 14:56:28 -0500 Subject: [PATCH 046/117] CoAuthor construction --- test/builder/commit.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/builder/commit.js b/test/builder/commit.js index a68ada437b6..d9dd960bb77 100644 --- a/test/builder/commit.js +++ b/test/builder/commit.js @@ -46,6 +46,12 @@ class CommitBuilder { this._multiFileDiff = builder.build().multiFilePatch; return this; } + + addCoAuthor(name, email) { + this._coAuthors.push({name, email}); + return this; + } + build() { const commit = new Commit({ sha: this._sha, From 942e7dccda77c95aed4e1a08713e292eaf8c5ec1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 14:56:41 -0500 Subject: [PATCH 047/117] PropTypes shuffle :dancer: --- lib/views/commit-detail-view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 96b38f1ec61..ec0416a57be 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -4,10 +4,13 @@ import {emojify} from 'node-emoji'; import moment from 'moment'; import MultiFilePatchController from '../controllers/multi-file-patch-controller'; +import CommitDetailItem from '../items/commit-detail-item'; export default class CommitDetailView extends React.Component { static propTypes = { repository: PropTypes.object.isRequired, + commit: PropTypes.object.isRequired, + itemType: PropTypes.oneOf([CommitDetailItem]).isRequired, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, @@ -16,7 +19,6 @@ export default class CommitDetailView extends React.Component { config: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, - commit: PropTypes.object.isRequired, } render() { From 1a28b7d3a7b3a62615690b605dc8ad54367e1a0d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 14:56:59 -0500 Subject: [PATCH 048/117] Ported CommitDetailView tests, all passing --- test/views/commit-detail-view.test.js | 127 +++++++++++++++----------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js index 731a5b71187..0dfc18ef132 100644 --- a/test/views/commit-detail-view.test.js +++ b/test/views/commit-detail-view.test.js @@ -1,69 +1,90 @@ import React from 'react'; import {shallow} from 'enzyme'; +import moment from 'moment'; + +import CommitDetailView from '../../lib/views/commit-detail-view'; +import CommitDetailItem from '../../lib/items/commit-detail-item'; +import {cloneRepository, buildRepository} from '../helpers'; +import {commitBuilder} from '../builder/commit'; describe('CommitDetailView', function() { - it('has a MultiFilePatchController that its itemType set'); + let repository, atomEnv; - it('passes unrecognized props to a MultiFilePatchController'); + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + repository = await buildRepository(await cloneRepository('multiple-commits')); + }); - it('renders commit details properly'); + afterEach(function() { + atomEnv.destroy(); + }); - it('renders multiple avatars for co-authored commit'); -}); + function buildApp(override = {}) { + const props = { + repository, + commit: commitBuilder().build(), + itemType: CommitDetailItem, -/* -it('has a MultiFilePatchController that has `disableStageUnstage` flag set to true', function() { - const wrapper = mount(buildApp()); - assert.isTrue(wrapper.find('MultiFilePatchController').exists()); - assert.isTrue(wrapper.find('MultiFilePatchController').prop('disableStageUnstage')); -}); + workspace: atomEnv.workspace, + commands: atomEnv.commands, + keymaps: atomEnv.keymaps, + tooltips: atomEnv.tooltips, + config: atomEnv.config, -it('passes unrecognized props to a MultiFilePatchController', function() { - const extra = Symbol('extra'); - const wrapper = shallow(buildApp({extra})); + destroy: () => {}, + ...override, + }; - assert.strictEqual(wrapper.find('MultiFilePatchController').prop('extra'), extra); -}); + return ; + } -it('renders commit details properly', function() { - const newCommit = new Commit({ - sha: '420', - authorEmail: 'very@nice.com', - authorDate: moment().subtract(2, 'days').unix(), - messageSubject: 'subject', - messageBody: 'messageBody', + it('has a MultiFilePatchController that its itemType set', function() { + const wrapper = shallow(buildApp({itemType: CommitDetailItem})); + assert.strictEqual(wrapper.find('MultiFilePatchController').prop('itemType'), CommitDetailItem); }); - const {multiFilePatch: mfp} = multiFilePatchBuilder().addFilePatch().build(); - sinon.stub(newCommit, 'getMultiFileDiff').returns(mfp); - const wrapper = mount(buildApp({commit: newCommit})); - assert.strictEqual(wrapper.find('.github-CommitDetailView-title').text(), 'subject'); - assert.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), 'messageBody'); - assert.strictEqual(wrapper.find('.github-CommitDetailView-metaText').text(), 'very@nice.com committed 2 days ago'); - assert.strictEqual(wrapper.find('.github-CommitDetailView-sha').text(), '420'); - // assert.strictEqual(wrapper.find('.github-CommitDetailView-sha a').prop('href'), '420'); - assert.strictEqual(wrapper.find('img.github-RecentCommit-avatar').prop('src'), 'https://avatars.githubusercontent.com/u/e?email=very%40nice.com&s=32'); -}); - -it('renders multiple avatars for co-authored commit', function() { - const newCommit = new Commit({ - sha: '420', - authorEmail: 'very@nice.com', - authorDate: moment().subtract(2, 'days').unix(), - messageSubject: 'subject', - messageBody: 'messageBody', - coAuthors: [{name: 'two', email: 'two@coauthor.com'}, {name: 'three', email: 'three@coauthor.com'}], + it('passes unrecognized props to a MultiFilePatchController', function() { + const extra = Symbol('extra'); + const wrapper = shallow(buildApp({extra})); + assert.strictEqual(wrapper.find('MultiFilePatchController').prop('extra'), extra); }); - const {multiFilePatch: mfp} = multiFilePatchBuilder().addFilePatch().build(); - sinon.stub(newCommit, 'getMultiFileDiff').returns(mfp); - const wrapper = mount(buildApp({commit: newCommit})); - assert.deepEqual( - wrapper.find('img.github-RecentCommit-avatar').map(w => w.prop('src')), - [ + + it('renders commit details properly', function() { + const commit = commitBuilder() + .sha('420') + .authorEmail('very@nice.com') + .authorDate(moment().subtract(2, 'days').unix()) + .messageSubject('subject') + .messageBody('body') + .setMultiFileDiff() + .build(); + const wrapper = shallow(buildApp({commit})); + + assert.strictEqual(wrapper.find('.github-CommitDetailView-title').text(), 'subject'); + assert.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), 'body'); + assert.strictEqual(wrapper.find('.github-CommitDetailView-metaText').text(), 'very@nice.com committed 2 days ago'); + assert.strictEqual(wrapper.find('.github-CommitDetailView-sha').text(), '420'); + // assert.strictEqual(wrapper.find('.github-CommitDetailView-sha a').prop('href'), '420'); + assert.strictEqual( + wrapper.find('img.github-RecentCommit-avatar').prop('src'), 'https://avatars.githubusercontent.com/u/e?email=very%40nice.com&s=32', - 'https://avatars.githubusercontent.com/u/e?email=two%40coauthor.com&s=32', - 'https://avatars.githubusercontent.com/u/e?email=three%40coauthor.com&s=32', - ], - ); + ); + }); + + it('renders multiple avatars for co-authored commit', function() { + const commit = commitBuilder() + .authorEmail('blaze@it.com') + .addCoAuthor('two', 'two@coauthor.com') + .addCoAuthor('three', 'three@coauthor.com') + .build(); + const wrapper = shallow(buildApp({commit})); + assert.deepEqual( + wrapper.find('img.github-RecentCommit-avatar').map(w => w.prop('src')), + [ + 'https://avatars.githubusercontent.com/u/e?email=blaze%40it.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=two%40coauthor.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=three%40coauthor.com&s=32', + ], + ); + }); }); -*/ From 4d8737cbc321cddefddecd98ed34e6e8304613fa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 15:48:33 -0500 Subject: [PATCH 049/117] Failing tests and initial implementation of abbreviatedBody() --- lib/models/commit.js | 39 +++++++++++++ test/models/commit.test.js | 112 +++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/lib/models/commit.js b/lib/models/commit.js index c568de80665..fb6433ace5e 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -3,6 +3,8 @@ const UNBORN = Symbol('unborn'); export default class Commit { static LONG_MESSAGE_THRESHOLD = 1000; + static BOUNDARY_SEARCH_THRESHOLD = 100; + static createUnborn() { return new Commit({unbornRef: UNBORN}); } @@ -50,6 +52,43 @@ export default class Commit { return `${this.getMessageSubject()}\n\n${this.getMessageBody()}`.trim(); } + /* + * Return the messageBody, truncated before the character at LONG_MESSAGE_THRESHOLD. If a paragraph boundary is + * found within BOUNDARY_SEARCH_THRESHOLD characters before that position, the message will be truncated at the + * end of the previous paragraph. If there is no paragraph boundary found, but a word boundary is found within + * that range, the text is truncated at that word boundary and an elipsis (...) is added. If neither are found, + * the text is truncated hard at LONG_MESSAGE_THRESHOLD - 3 characters and an elipsis (...) is added. + */ + abbreviatedBody() { + if (!this.isBodyLong()) { + return this.getMessageBody(); + } + + const {LONG_MESSAGE_THRESHOLD, BOUNDARY_SEARCH_THRESHOLD} = this.constructor; + let elipsis = '...'; + let lastParagraphIndex = Infinity; + let lastWordIndex = Infinity; + + const boundarySearch = this.getMessageBody() + .substring(LONG_MESSAGE_THRESHOLD - BOUNDARY_SEARCH_THRESHOLD, LONG_MESSAGE_THRESHOLD); + + const boundaryRx = /\r?\n\r?\n|\s+/g; + let result; + while ((result = boundaryRx.exec(boundarySearch)) !== null) { + if (/\r?\n\r?\n/.test(result[0])) { + // Paragraph boundary. Omit the elipsis + lastParagraphIndex = result.index; + elipsis = ''; + } else if (result.index <= BOUNDARY_SEARCH_THRESHOLD - elipsis.length) { + // Word boundary. Only count if we have room for the elipsis under the cutoff. + lastWordIndex = result.index; + } + } + + const cutoffIndex = Math.min(lastParagraphIndex, lastWordIndex); + return this.getMessageBody().substring(0, cutoffIndex) + elipsis; + } + setMultiFileDiff(multiFileDiff) { this.multiFileDiff = multiFileDiff; } diff --git a/test/models/commit.test.js b/test/models/commit.test.js index 1adb497f100..9e3e7664eab 100644 --- a/test/models/commit.test.js +++ b/test/models/commit.test.js @@ -38,4 +38,116 @@ describe('Commit', function() { assert.isFalse(nullCommit.isBodyLong()); }); }); + + describe('abbreviatedBody()', function() { + it('returns the message body as-is when the body is short', function() { + const commit = commitBuilder().messageBody('short').build(); + assert.strictEqual(commit.abbreviatedBody(), 'short'); + }); + + it('truncates the message body at the nearest paragraph boundary before the cutoff if one is nearby', function() { + // The | is at the 1000-character mark. + const body = dedent` + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, + essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. + + Ea salutatus contentiones eos. Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere + urbanitas, usu ut aperiri mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, + scripta iudicabit ne nam, in duis clita commodo sit. + + Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et eum + voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus + tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam + reprehendunt et mea. Ea eius omnes voluptua sit. + + No cum illud verear efficiantur. Id altera imperdiet nec. Noster aud|iam accusamus mei at, no zril libris nemore + duo, ius ne rebum doctus fuisset. Legimus epicurei in sit, esse purto suscipit eu qui, oporteat deserunt + delicatissimi sea in. Est id putent accusata convenire, no tibique molestie accommodare quo, cu est fuisset + offendit evertitur. + `; + + const commit = commitBuilder().messageBody(body).build(); + assert.strictEqual(commit.abbreviatedBody(), dedent` + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, + essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. + + Ea salutatus contentiones eos. Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere + urbanitas, usu ut aperiri mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, + scripta iudicabit ne nam, in duis clita commodo sit. + + Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et eum + voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus + tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam + reprehendunt et mea. Ea eius omnes voluptua sit. + `); + }); + + it('truncates the message body at the nearest word boundary before the cutoff if one is nearby', function() { + // The | is at the 1000-character mark. + const body = dedent` + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. + + Mazim alterum sea ea, essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore + albucius te vis, eam tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. + Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri + mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, scripta iudicabit ne nam, in + duis clita commodo sit. Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat + comprehensam ut his, et eum voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei + liber putant. Ad doctus tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent + philosophia et vix. Nusquam reprehendunt et mea. Ea eius omnes voluptua sit. No cum illud verear efficiantur. Id + altera imperdiet nec. Noster audia|m accusamus mei at, no zril libris nemore duo, ius ne rebum doctus fuisset. + Legimus epicurei in sit, esse purto suscipit eu qui, oporteat deserunt delicatissimi sea in. Est id putent + accusata convenire, no tibique molestie accommodare quo, cu est fuisset offendit evertitur. + `; + + const commit = commitBuilder().messageBody(body).build(); + assert.strictEqual(commit.abbreviatedBody(), dedent` + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. + + Mazim alterum sea ea, essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore + albucius te vis, eam tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. + Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri + mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, scripta iudicabit ne nam, in + duis clita commodo sit. Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat + comprehensam ut his, et eum voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei + liber putant. Ad doctus tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent + philosophia et vix. Nusquam reprehendunt et mea. Ea eius omnes voluptua sit. No cum illud verear efficiantur. Id + altera imperdiet nec. Noster... + `); + }); + + it('truncates the message body at the character cutoff if no word or paragraph boundaries can be found', function() { + // The | is at the 1000-character mark. + const body = 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.\n\n' + + 'Mazim alterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' + + 'albuciustevis,eamtantasnullamcorrumpitad,inoratioluptatumeleifendvim.Easalutatuscontentioneseos.' + + 'Eaminveniamfacetevolutpat,solumappetereadversariumutquo.Velcuappetereurbanitas,usuutaperiri' + + 'mediocritatem,aliamolestieurbanitascuqui.Velitantiopamerroribusnoeum,scriptaiudicabitnenam,in' + + 'duisclitacommodosit.Assumsensibusoporteretevel,vissemperevertiturdefiniebasin.Tamquamfeugiat' + + 'comprehensamuthis,eteumvoluptuaullamcorper,exmeidebitisinciderint.Sitdiscerepertinaxte,anmei' + + 'liberputant.Addoctustractatosius,duoadcivibusalienum,nominativoluptariasedan.Librisessent' + + 'philosophiaetvix.Nusquamreprehenduntetmea.Eaeiusomnesvoluptuasit.Nocumilludverearefficiantur.Id' + + 'alteraimperdietnec.Nosteraudiamaccusamusmeiat,nozrillibrisnemoreduo,iusnerebumdoctusfuisset.' + + 'Legimusepicureiinsit,essepurtosuscipiteuqui,oporteatdeseruntdelicatissimiseain.Estidputent' + + '|accusataconvenire,notibiquemolestieaccommodarequo,cuestfuissetoffenditevertitur.'; + + const commit = commitBuilder().messageBody(body).build(); + assert.strictEqual( + commit.abbreviatedBody(), + 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.\n\n' + + 'Mazim alterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' + + 'albuciustevis,eamtantasnullamcorrumpitad,inoratioluptatumeleifendvim.Easalutatuscontentioneseos.' + + 'Eaminveniamfacetevolutpat,solumappetereadversariumutquo.Velcuappetereurbanitas,usuutaperiri' + + 'mediocritatem,aliamolestieurbanitascuqui.Velitantiopamerroribusnoeum,scriptaiudicabitnenam,in' + + 'duisclitacommodosit.Assumsensibusoporteretevel,vissemperevertiturdefiniebasin.Tamquamfeugiat' + + 'comprehensamuthis,eteumvoluptuaullamcorper,exmeidebitisinciderint.Sitdiscerepertinaxte,anmei' + + 'liberputant.Addoctustractatosius,duoadcivibusalienum,nominativoluptariasedan.Librisessent' + + 'philosophiaetvix.Nusquamreprehenduntetmea.Eaeiusomnesvoluptuasit.Nocumilludverearefficiantur.Id' + + 'alteraimperdietnec.Nosteraudiamaccusamusmeiat,nozrillibrisnemoreduo,iusnerebumdoctusfuisset.' + + 'Legimusepicureiinsit,essepurtosuscipiteuqui,oporteatdeseruntdelicatissimiseain.Estidput...', + ); + }); + }); }); From 4ceba4b4ca61ae6e4da2218b9cb00e0bd0fc5d62 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Nov 2018 15:52:06 -0500 Subject: [PATCH 050/117] Pending tests for collapsing and uncollapsing message bodies --- test/views/commit-detail-view.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js index 0dfc18ef132..357505912e0 100644 --- a/test/views/commit-detail-view.test.js +++ b/test/views/commit-detail-view.test.js @@ -23,6 +23,8 @@ describe('CommitDetailView', function() { const props = { repository, commit: commitBuilder().build(), + messageCollapsible: false, + messageOpen: true, itemType: CommitDetailItem, workspace: atomEnv.workspace, @@ -87,4 +89,14 @@ describe('CommitDetailView', function() { ], ); }); + + describe('commit message collapsibility', function() { + it('renders the full message when messageCollapsible is false'); + + it('renders an abbreviated message when messageCollapsible is true and messageOpen is false'); + + it('renders the full message when messageCollapsible is true and messageOpen is true'); + + it('calls toggleMessage the "See More" or "See Less" buttons are clicked'); + }); }); From 38b0fa9e40b40e6f9a8c1034c1d7f5b6e700e0e3 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 13:11:40 -0800 Subject: [PATCH 051/117] :fire: autoHeight as a prop since it's always false --- lib/controllers/commit-preview-controller.js | 1 - lib/controllers/multi-file-patch-controller.js | 1 - lib/views/multi-file-patch-view.js | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/controllers/commit-preview-controller.js b/lib/controllers/commit-preview-controller.js index ff5f3cf72ba..f1ce3c988c7 100644 --- a/lib/controllers/commit-preview-controller.js +++ b/lib/controllers/commit-preview-controller.js @@ -23,7 +23,6 @@ export default class CommitPreviewController extends React.Component { return ( ); diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 667184aae44..cdba88def62 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -25,7 +25,6 @@ export default class MultiFilePatchController extends React.Component { discardLines: PropTypes.func, undoLastDiscard: PropTypes.func, surface: PropTypes.func, - autoHeight: PropTypes.bool, } constructor(props) { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 5e3d3d73bfc..5e35e5033bf 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -58,7 +58,6 @@ export default class MultiFilePatchView extends React.Component { toggleSymlinkChange: PropTypes.func, undoLastDiscard: PropTypes.func, discardRows: PropTypes.func, - autoHeight: PropTypes.bool, refInitialFocus: RefHolderPropType, itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, } @@ -241,7 +240,7 @@ export default class MultiFilePatchView extends React.Component { buffer={this.props.multiFilePatch.getBuffer()} lineNumberGutterVisible={false} autoWidth={false} - autoHeight={this.props.autoHeight} + autoHeight={false} readOnly={true} softWrapped={true} From 205c43754631120585824ac433e8fbd844469156 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 14:42:02 -0800 Subject: [PATCH 052/117] collapse and uncollapse commit message bodies --- lib/views/commit-detail-view.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index ec0416a57be..dafdcb1123f 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -19,6 +19,10 @@ export default class CommitDetailView extends React.Component { config: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, + + messageCollapsible: PropTypes.bool.isRequired, + messageOpen: PropTypes.bool.isRequired, + toggleMessage: PropTypes.func.isRequired, } render() { @@ -33,9 +37,9 @@ export default class CommitDetailView extends React.Component {

    {emojify(commit.getMessageSubject())} + {this.renderShowMoreButton()}

    -
    -                {emojify(commit.getMessageBody())}
    + {this.renderCommitMessageBody(commit)}
    {/* TODO fix image src */} {this.renderAuthors()} @@ -62,6 +66,25 @@ export default class CommitDetailView extends React.Component { ); } + renderCommitMessageBody(commit) { + if (this.props.messageOpen || !this.props.messageCollapsible) { + return ( +
    +          {emojify(commit.getMessageBody())}
    + ); + } + } + + renderShowMoreButton() { + if (!this.props.messageCollapsible) { + return null; + } + const buttonText = this.props.messageOpen ? 'Hide More' : 'Show More'; + return ( + + ); + } + humanizeTimeSince(date) { return moment(date * 1000).fromNow(); } From ec10f482e22823995def2968eb11d6451c5a16d4 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 14:45:53 -0800 Subject: [PATCH 053/117] return null if we're not gonna render the commit message body --- lib/views/commit-detail-view.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index dafdcb1123f..11a3981f992 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -72,6 +72,8 @@ export default class CommitDetailView extends React.Component {
               {emojify(commit.getMessageBody())}
    ); + } else { + return null; } } From e1102d07bd0cdc2f5b31ca4242edd3fea453e659 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 14:56:19 -0800 Subject: [PATCH 054/117] style the button --- lib/views/commit-detail-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 11a3981f992..b965f70eb2e 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -83,7 +83,7 @@ export default class CommitDetailView extends React.Component { } const buttonText = this.props.messageOpen ? 'Hide More' : 'Show More'; return ( - + ); } From 1af8a5d119256a46e4822acece992a7b6f0153fb Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 16:51:35 -0800 Subject: [PATCH 055/117] :fire: some dead code --- lib/views/commit-detail-view.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index b965f70eb2e..125be8778b8 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -27,8 +27,6 @@ export default class CommitDetailView extends React.Component { render() { const commit = this.props.commit; - // const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; - // const {avatarUrl, name, date} = this.props.item.committer; return (
    From 6a3d65cc7fe8584e5747ec5f826f8b75da6b153b Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 16:53:56 -0800 Subject: [PATCH 056/117] unit tests for commit message collapsibility --- test/views/commit-detail-view.test.js | 63 +++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js index 357505912e0..ee39d83e400 100644 --- a/test/views/commit-detail-view.test.js +++ b/test/views/commit-detail-view.test.js @@ -1,6 +1,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import moment from 'moment'; +import dedent from 'dedent-js'; import CommitDetailView from '../../lib/views/commit-detail-view'; import CommitDetailItem from '../../lib/items/commit-detail-item'; @@ -34,6 +35,7 @@ describe('CommitDetailView', function() { config: atomEnv.config, destroy: () => {}, + toggleMessage: () => {}, ...override, }; @@ -91,12 +93,65 @@ describe('CommitDetailView', function() { }); describe('commit message collapsibility', function() { - it('renders the full message when messageCollapsible is false'); + let wrapper; + const commitMessageBody = dedent` + if every pork chop was perfect... - it('renders an abbreviated message when messageCollapsible is true and messageOpen is false'); - it('renders the full message when messageCollapsible is true and messageOpen is true'); - it('calls toggleMessage the "See More" or "See Less" buttons are clicked'); + we wouldn't have hot dogs! + 🌭🌭🌭🌭🌭🌭🌭 + `; + const commit = commitBuilder() + .authorEmail('greg@mruniverse.biz') + .messageBody(commitMessageBody) + .build(); + describe('when messageCollapsible is false', function() { + beforeEach(function() { + wrapper = shallow(buildApp({commit, messageCollapsible: false})); + }); + it('renders the full message body', function() { + assert.deepEqual(wrapper.find('.github-CommitDetailView-moreText').text(), commitMessageBody); + }); + it('does not render a button', function() { + + }); + }); + describe('when messageCollapsible is true and messageOpen is false', function() { + beforeEach(function() { + wrapper = shallow(buildApp({commit, messageCollapsible: true, messageOpen: false})); + }); + it('does not render commit message', function() { + assert.lengthOf(wrapper.find('.github-CommitDetailView-moreText'), 0); + }); + + it('renders button with the text `Show More`', function() { + const button = wrapper.find('.github-CommitDetailView-moreButton'); + assert.lengthOf(button, 1); + assert.deepEqual(button.text(), 'Show More'); + }); + }); + + describe('when messageCollapsible is true and messageOpen is true', function() { + let toggleMessage; + beforeEach(function() { + toggleMessage = sinon.spy(); + wrapper = shallow(buildApp({commit, messageCollapsible: true, messageOpen: true, toggleMessage})); + }); + it('renders the full message', function() { + assert.deepEqual(wrapper.find('.github-CommitDetailView-moreText').text(), commitMessageBody); + }); + it('renders a button with the text `Hide More`', function() { + const button = wrapper.find('.github-CommitDetailView-moreButton'); + assert.lengthOf(button, 1); + assert.deepEqual(button.text(), 'Hide More'); + }); + it('button calls `toggleMessage` prop when clicked', function() { + const button = wrapper.find('.github-CommitDetailView-moreButton'); + button.simulate('click'); + assert.ok(toggleMessage.called); + }); + + }); }); }); From 8e7561f6808e1ffd9f4584524d5a641e836dbb98 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 16:57:28 -0800 Subject: [PATCH 057/117] :fire: unnecessary console logging --- lib/views/pr-commit-view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/views/pr-commit-view.js b/lib/views/pr-commit-view.js index 1d31e5947da..88d879a3cbf 100644 --- a/lib/views/pr-commit-view.js +++ b/lib/views/pr-commit-view.js @@ -38,7 +38,6 @@ export class PrCommitView extends React.Component { } render() { - console.log('zzz'); const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; const {avatarUrl, name, date} = this.props.item.committer; return ( From 414390db1fe765fb12c2fdb3bb1547f3ddeae434 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 28 Nov 2018 17:16:51 -0800 Subject: [PATCH 058/117] don't render staging/unstaging buttons for mode and symlink changes --- lib/views/file-patch-meta-view.js | 28 +++++++++++++++++++++------- lib/views/multi-file-patch-view.js | 5 +++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/views/file-patch-meta-view.js b/lib/views/file-patch-meta-view.js index bbefd913f6d..fadc45edd88 100644 --- a/lib/views/file-patch-meta-view.js +++ b/lib/views/file-patch-meta-view.js @@ -1,6 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import CommitDetailItem from '../items/commit-detail-item'; +import ChangedFileItem from '../items/changed-file-item'; +import CommitPreviewItem from '../items/commit-preview-item'; export default class FilePatchMetaView extends React.Component { static propTypes = { @@ -11,21 +14,32 @@ export default class FilePatchMetaView extends React.Component { action: PropTypes.func.isRequired, children: PropTypes.element.isRequired, + itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired, }; + renderMetaControls() { + console.log(this.props); + if (this.props.itemType === CommitDetailItem) { + return null; + } + return ( +
    + +
    + ); + } + render() { return (

    {this.props.title}

    -
    - -
    + {this.renderMetaControls()}
    {this.props.children} diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 5e35e5033bf..61414c30a3b 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -361,6 +361,7 @@ export default class MultiFilePatchView extends React.Component { title="Mode change" actionIcon={attrs.actionIcon} actionText={attrs.actionText} + itemType={this.props.itemType} action={() => this.props.toggleModeChange(filePatch)}> File changed mode @@ -444,6 +445,7 @@ export default class MultiFilePatchView extends React.Component { title={title} actionIcon={attrs.actionIcon} actionText={attrs.actionText} + itemType={this.props.itemType} action={() => this.props.toggleSymlinkChange(filePatch)}> {detail} @@ -453,6 +455,9 @@ export default class MultiFilePatchView extends React.Component { } renderHunkHeaders(filePatch) { + if (this.props.itemType === CommitDetailItem) { + return null; + } const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; const selectedHunks = new Set( Array.from(this.props.selectedRows, row => this.props.multiFilePatch.getHunkAt(row)), From 1467db2a5af9ece794d6d67767c8b8adfb1c6aab Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 29 Nov 2018 11:24:29 +0900 Subject: [PATCH 059/117] Wrap whitespace in commit body --- styles/commit-detail.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/commit-detail.less b/styles/commit-detail.less index 6f8c24f975a..b3c3ef8c006 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -69,7 +69,7 @@ font-family: var(--editor-font-family); word-wrap: initial; word-break: break-word; - white-space: initial; + white-space: pre-wrap; background-color: transparent; &:empty { display: none; From e1de0771e7c6d9c267381d5f1cc27e9d884bbf32 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 29 Nov 2018 12:36:04 +0900 Subject: [PATCH 060/117] Move sha into CommitDetailView-meta --- lib/views/commit-detail-view.js | 38 ++++++++++++++++----------------- styles/commit-detail.less | 21 +++++++++--------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 125be8778b8..a3b924a3adf 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -31,28 +31,26 @@ export default class CommitDetailView extends React.Component { return (
    -
    -
    -

    - {emojify(commit.getMessageSubject())} - {this.renderShowMoreButton()} -

    - {this.renderCommitMessageBody(commit)} -
    - {/* TODO fix image src */} - {this.renderAuthors()} - - {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} - +
    +

    + {emojify(commit.getMessageSubject())} + {this.renderShowMoreButton()} +

    + {this.renderCommitMessageBody(commit)} +
    + {/* TODO fix image src */} + {this.renderAuthors()} + + {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} + +
    + {/* TODO fix href */} + + {commit.getSha()} +
    -
    - {/* TODO fix href */} - - {commit.getSha()} - -
    Date: Thu, 29 Nov 2018 14:10:34 +0900 Subject: [PATCH 061/117] Restyle header --- styles/commit-detail.less | 7 ++----- styles/file-patch-view.less | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/styles/commit-detail.less b/styles/commit-detail.less index bc5cddb5097..e6cab0bf147 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -10,15 +10,12 @@ &-header { flex: 0; - padding: @default-padding; - padding-bottom: 0; + border-bottom: 1px solid @base-border-color; background-color: @syntax-background-color; } &-commit { - padding: @default-padding; - border: 1px solid @base-border-color; - border-radius: @component-border-radius; + padding: @default-padding*2; } &-title { diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index fcf9a7cb485..830fadc1e44 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -24,7 +24,7 @@ } .github-FilePatchView-controlBlock { - padding: @component-padding*2 @component-padding @component-padding 0; + padding: @component-padding*4 @component-padding @component-padding 0; background-color: @syntax-background-color; & + .github-FilePatchView-controlBlock { From 2cc14b1b17973cbd10d459fdd9ba18864e7f6c12 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 28 Nov 2018 23:38:05 +0100 Subject: [PATCH 062/117] add test to ensure buttons don't get rendered when inside a CommitDetailItem --- lib/views/hunk-header-view.js | 2 +- test/views/file-patch-header-view.test.js | 6 ++++++ test/views/hunk-header-view.test.js | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index 6fd986ee542..ab2a43ae1b0 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -58,7 +58,7 @@ export default class HunkHeaderView extends React.Component { } renderButtons() { - if (this.props.itemType === CommitPreviewItem) { + if (this.props.itemType === CommitPreviewItem || this.props.itemType === CommitDetailItem) { return null; } else { return ( diff --git a/test/views/file-patch-header-view.test.js b/test/views/file-patch-header-view.test.js index 2d4e0acb651..cf778723cb9 100644 --- a/test/views/file-patch-header-view.test.js +++ b/test/views/file-patch-header-view.test.js @@ -5,6 +5,7 @@ import path from 'path'; import FilePatchHeaderView from '../../lib/views/file-patch-header-view'; import ChangedFileItem from '../../lib/items/changed-file-item'; import CommitPreviewItem from '../../lib/items/commit-preview-item'; +import CommitDetailItem from '../../lib/items/commit-detail-item'; describe('FilePatchHeaderView', function() { const relPath = path.join('dir', 'a.txt'); @@ -178,5 +179,10 @@ describe('FilePatchHeaderView', function() { buttonClass: 'icon-move-up', oppositeButtonClass: 'icon-move-down', })); + + it('does not render buttons when in a CommitDetailItem', function() { + const wrapper = shallow(buildApp({itemType: CommitDetailItem})); + assert.isFalse(wrapper.find('.btn-group').exists()); + }); }); }); diff --git a/test/views/hunk-header-view.test.js b/test/views/hunk-header-view.test.js index ae1b80fefe6..78c263ae813 100644 --- a/test/views/hunk-header-view.test.js +++ b/test/views/hunk-header-view.test.js @@ -4,6 +4,8 @@ import {shallow} from 'enzyme'; import HunkHeaderView from '../../lib/views/hunk-header-view'; import RefHolder from '../../lib/models/ref-holder'; import Hunk from '../../lib/models/patch/hunk'; +import CommitDetailItem from '../../lib/items/commit-detail-item'; +import CommitPreviewItem from '../../lib/items/commit-preview-item'; describe('HunkHeaderView', function() { let atomEnv, hunk; @@ -117,4 +119,14 @@ describe('HunkHeaderView', function() { assert.isFalse(mouseDown.called); assert.isTrue(evt.stopPropagation.called); }); + + it('does not render extra buttons when in a CommitPreviewItem or a CommitDetailItem', function() { + let wrapper = shallow(buildApp({itemType: CommitPreviewItem})); + assert.isFalse(wrapper.find('.github-HunkHeaderView-stageButton').exists()); + assert.isFalse(wrapper.find('.github-HunkHeaderView-discardButton').exists()); + + wrapper = shallow(buildApp({itemType: CommitDetailItem})); + assert.isFalse(wrapper.find('.github-HunkHeaderView-stageButton').exists()); + assert.isFalse(wrapper.find('.github-HunkHeaderView-discardButton').exists()); + }) }); From e0d97abd00e26b87a49f0bf9a752e5cafb7de458 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 10:34:37 -0500 Subject: [PATCH 063/117] Fixed it all on the first go apparently That never happens --- lib/models/commit.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/models/commit.js b/lib/models/commit.js index fb6433ace5e..3e7e65b3d41 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -65,28 +65,29 @@ export default class Commit { } const {LONG_MESSAGE_THRESHOLD, BOUNDARY_SEARCH_THRESHOLD} = this.constructor; - let elipsis = '...'; + let elipses = '...'; let lastParagraphIndex = Infinity; let lastWordIndex = Infinity; + const lastSubwordIndex = BOUNDARY_SEARCH_THRESHOLD - elipses.length; - const boundarySearch = this.getMessageBody() - .substring(LONG_MESSAGE_THRESHOLD - BOUNDARY_SEARCH_THRESHOLD, LONG_MESSAGE_THRESHOLD); + const baseIndex = LONG_MESSAGE_THRESHOLD - BOUNDARY_SEARCH_THRESHOLD; + const boundarySearch = this.getMessageBody().substring(baseIndex, LONG_MESSAGE_THRESHOLD); const boundaryRx = /\r?\n\r?\n|\s+/g; let result; while ((result = boundaryRx.exec(boundarySearch)) !== null) { if (/\r?\n\r?\n/.test(result[0])) { - // Paragraph boundary. Omit the elipsis + // Paragraph boundary. Omit the elipses lastParagraphIndex = result.index; - elipsis = ''; - } else if (result.index <= BOUNDARY_SEARCH_THRESHOLD - elipsis.length) { - // Word boundary. Only count if we have room for the elipsis under the cutoff. + elipses = ''; + } else if (result.index <= BOUNDARY_SEARCH_THRESHOLD - elipses.length) { + // Word boundary. Only count if we have room for the elipses under the cutoff. lastWordIndex = result.index; } } - const cutoffIndex = Math.min(lastParagraphIndex, lastWordIndex); - return this.getMessageBody().substring(0, cutoffIndex) + elipsis; + const cutoffIndex = baseIndex + Math.min(lastParagraphIndex, lastWordIndex, lastSubwordIndex); + return this.getMessageBody().substring(0, cutoffIndex) + elipses; } setMultiFileDiff(multiFileDiff) { From 94b5b960bd6018d3b6a38ab6989d8e49789013d6 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 16:29:14 +0100 Subject: [PATCH 064/117] `getRemoteForBranch` now gets the remote from its remoteSet --- lib/models/repository.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/models/repository.js b/lib/models/repository.js index 68202d136e2..dbb238faebd 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -214,11 +214,7 @@ export default class Repository { async getRemoteForBranch(branchName) { const name = await this.getConfig(`branch.${branchName}.remote`); - if (name === null) { - return nullRemote; - } else { - return new Remote(name); - } + return (await this.getRemotes()).withName(name); } async saveDiscardHistory() { From 6ee4dd4a6df3e340ce6f5a086045d4592769fb59 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 16:38:02 +0100 Subject: [PATCH 065/117] remove unused improts --- lib/models/repository.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/models/repository.js b/lib/models/repository.js index dbb238faebd..81104895a92 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -5,7 +5,6 @@ import fs from 'fs-extra'; import {getNullActionPipelineManager} from '../action-pipeline'; import CompositeGitStrategy from '../composite-git-strategy'; -import Remote, {nullRemote} from './remote'; import Author, {nullAuthor} from './author'; import Branch from './branch'; import {Loading, Absent, LoadingGuess, AbsentGuess} from './repository-states'; From 4655dd303f756f5a5745383b3ab53510ad927c41 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 16:53:56 +0100 Subject: [PATCH 066/117] [wip] render dotcom link --- lib/containers/commit-detail-container.js | 3 +++ lib/models/repository-states/present.js | 4 +++ lib/models/repository-states/state.js | 4 +++ lib/models/repository.js | 1 + lib/views/commit-detail-view.js | 30 +++++++++++++++++------ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/containers/commit-detail-container.js b/lib/containers/commit-detail-container.js index 911c73b5f8f..275d82ce353 100644 --- a/lib/containers/commit-detail-container.js +++ b/lib/containers/commit-detail-container.js @@ -15,6 +15,9 @@ export default class CommitDetailContainer extends React.Component { fetchData = repository => { return yubikiri({ commit: repository.getCommit(this.props.sha), + currentBranch: repository.getCurrentBranch(), + currentRemote: async query => repository.getRemoteForBranch((await query.currentBranch).getName()), + isCommitPushed: repository.isCommitPushed(this.props.sha), }); } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index bc285890810..6eb53323e60 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -741,6 +741,10 @@ export default class Present extends State { }); } + isCommitPushed({sha}) { + return true; + } + // Author information getAuthors(options) { diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 92961a777ad..82c443a255e 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -306,6 +306,10 @@ export default class State { return Promise.resolve([]); } + isCommitPushed({sha}) { + return false; + } + // Author information getAuthors() { diff --git a/lib/models/repository.js b/lib/models/repository.js index 81104895a92..e4960aed2af 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -328,6 +328,7 @@ const delegates = [ 'getLastCommit', 'getCommit', 'getRecentCommits', + 'isCommitPushed', 'getAuthors', diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index a3b924a3adf..487ffa5c7b6 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -23,6 +23,10 @@ export default class CommitDetailView extends React.Component { messageCollapsible: PropTypes.bool.isRequired, messageOpen: PropTypes.bool.isRequired, toggleMessage: PropTypes.func.isRequired, + + currentRemote: PropTypes.object.isRequired, + currentBranch: PropTypes.object.isRequired, + isCommitPushed: PropTypes.bool.isRequired, } render() { @@ -43,13 +47,7 @@ export default class CommitDetailView extends React.Component { {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} -
    - {/* TODO fix href */} - - {commit.getSha()} - -
    + {this.renderDotComLink()}
    @@ -87,6 +85,24 @@ export default class CommitDetailView extends React.Component { return moment(date * 1000).fromNow(); } + renderDotComLink() { + const remote = this.props.currentRemote; + const sha = this.props.commit.getSha(); + if (remote && remote.isGithubRepo() && this.props.isCommitPushed) { + const repoUrl = `https://www.github.com/${this.props.currentRemote.getOwner()}/${this.props.currentRemote.getRepo()}`; + return ( + + ); + } else { + return null; + } + } + getAuthorInfo() { const coAuthorCount = this.props.commit.getCoAuthors().length; return coAuthorCount ? this.props.commit.getAuthorEmail() : `${coAuthorCount + 1} people`; From da431290974773b1ccdede986aac2f0e6262ad6f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 11:19:00 -0500 Subject: [PATCH 067/117] Toggle between an abbreviated commit message body and the full one. --- lib/views/commit-detail-view.js | 28 ++++++------ test/views/commit-detail-view.test.js | 61 +++++++++++++++++---------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 487ffa5c7b6..9438c075c2b 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -36,11 +36,9 @@ export default class CommitDetailView extends React.Component {
    -

    - {emojify(commit.getMessageSubject())} - {this.renderShowMoreButton()} -

    - {this.renderCommitMessageBody(commit)} +

    {emojify(commit.getMessageSubject())}

    + {this.renderCommitMessageBody()} + {this.renderShowMoreButton()}
    {/* TODO fix image src */} {this.renderAuthors()} @@ -60,22 +58,22 @@ export default class CommitDetailView extends React.Component { ); } - renderCommitMessageBody(commit) { - if (this.props.messageOpen || !this.props.messageCollapsible) { - return ( -
    -          {emojify(commit.getMessageBody())}
    - ); - } else { - return null; - } + renderCommitMessageBody() { + const collapsed = this.props.messageCollapsible && !this.props.messageOpen; + + return ( +
    +        {collapsed ? this.props.commit.abbreviatedBody() : this.props.commit.getMessageBody()}
    +      
    + ); } renderShowMoreButton() { if (!this.props.messageCollapsible) { return null; } - const buttonText = this.props.messageOpen ? 'Hide More' : 'Show More'; + + const buttonText = this.props.messageOpen ? 'Show Less' : 'Show More'; return ( ); diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js index ee39d83e400..acca9a093d8 100644 --- a/test/views/commit-detail-view.test.js +++ b/test/views/commit-detail-view.test.js @@ -5,6 +5,7 @@ import dedent from 'dedent-js'; import CommitDetailView from '../../lib/views/commit-detail-view'; import CommitDetailItem from '../../lib/items/commit-detail-item'; +import Commit from '../../lib/models/commit'; import {cloneRepository, buildRepository} from '../helpers'; import {commitBuilder} from '../builder/commit'; @@ -93,65 +94,81 @@ describe('CommitDetailView', function() { }); describe('commit message collapsibility', function() { - let wrapper; - const commitMessageBody = dedent` - if every pork chop was perfect... + let wrapper, shortMessage, longMessage; + beforeEach(function() { + shortMessage = dedent` + if every pork chop was perfect... + we wouldn't have hot dogs! + 🌭🌭🌭🌭🌭🌭🌭 + `; + + longMessage = 'this message is really really really\n'; + while (longMessage.length < Commit.LONG_MESSAGE_THRESHOLD) { + longMessage += 'really really really really really really\n'; + } + longMessage += 'really really long.'; + }); - we wouldn't have hot dogs! - 🌭🌭🌭🌭🌭🌭🌭 - `; - const commit = commitBuilder() - .authorEmail('greg@mruniverse.biz') - .messageBody(commitMessageBody) - .build(); describe('when messageCollapsible is false', function() { beforeEach(function() { + const commit = commitBuilder().messageBody(shortMessage).build(); wrapper = shallow(buildApp({commit, messageCollapsible: false})); }); + it('renders the full message body', function() { - assert.deepEqual(wrapper.find('.github-CommitDetailView-moreText').text(), commitMessageBody); + assert.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), shortMessage); }); - it('does not render a button', function() { + it('does not render a button', function() { + assert.isFalse(wrapper.find('.github-CommitDetailView-moreButton').exists()); }); }); + describe('when messageCollapsible is true and messageOpen is false', function() { beforeEach(function() { + const commit = commitBuilder().messageBody(longMessage).build(); wrapper = shallow(buildApp({commit, messageCollapsible: true, messageOpen: false})); }); - it('does not render commit message', function() { - assert.lengthOf(wrapper.find('.github-CommitDetailView-moreText'), 0); + + it('renders an abbreviated commit message', function() { + const messageText = wrapper.find('.github-CommitDetailView-moreText').text(); + assert.notStrictEqual(messageText, longMessage); + assert.isAtMost(messageText.length, Commit.LONG_MESSAGE_THRESHOLD); }); - it('renders button with the text `Show More`', function() { + it('renders a button to reveal the rest of the message', function() { const button = wrapper.find('.github-CommitDetailView-moreButton'); assert.lengthOf(button, 1); - assert.deepEqual(button.text(), 'Show More'); + assert.strictEqual(button.text(), 'Show More'); }); }); describe('when messageCollapsible is true and messageOpen is true', function() { let toggleMessage; + beforeEach(function() { toggleMessage = sinon.spy(); + const commit = commitBuilder().messageBody(longMessage).build(); wrapper = shallow(buildApp({commit, messageCollapsible: true, messageOpen: true, toggleMessage})); }); + it('renders the full message', function() { - assert.deepEqual(wrapper.find('.github-CommitDetailView-moreText').text(), commitMessageBody); + assert.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), longMessage); }); - it('renders a button with the text `Hide More`', function() { + + it('renders a button to collapse the message text', function() { const button = wrapper.find('.github-CommitDetailView-moreButton'); assert.lengthOf(button, 1); - assert.deepEqual(button.text(), 'Hide More'); + assert.strictEqual(button.text(), 'Show Less'); }); - it('button calls `toggleMessage` prop when clicked', function() { + + it('the button calls toggleMessage when clicked', function() { const button = wrapper.find('.github-CommitDetailView-moreButton'); button.simulate('click'); - assert.ok(toggleMessage.called); + assert.isTrue(toggleMessage.called); }); - }); }); }); From c58c54047d4ad44fd7b25f6fa2e8f556b9d31959 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 17:24:26 +0100 Subject: [PATCH 068/117] fix: wrong conditional in hunk header --- lib/views/hunk-header-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index ab2a43ae1b0..a2ed357015f 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -58,7 +58,7 @@ export default class HunkHeaderView extends React.Component { } renderButtons() { - if (this.props.itemType === CommitPreviewItem || this.props.itemType === CommitDetailItem) { + if (this.props.itemType === CommitDetailItem) { return null; } else { return ( From e110282fa6fc206e771afa3bea539b0e16a1458d Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 17:47:28 +0100 Subject: [PATCH 069/117] add `getBranchesWithCommit` method --- lib/git-shell-out-strategy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 1addc791b31..5ea5d0ae56d 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -934,6 +934,11 @@ export default class GitShellOutStrategy { }); } + async getBranchesWithCommit(sha, {remotesOnly} = {}) { + const args = ['branch', ...(remotesOnly ? ['--remotes'] : []), '--format=%(refname:short)', '--contains', sha]; + return (await this.exec(args)).trim().split(LINE_ENDING_REGEX); + } + checkoutFiles(paths, revision) { if (paths.length === 0) { return null; } const args = ['checkout']; From 2184380a7c5cfc6cf9275e247e87e18c7b08e26b Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 17:57:41 +0100 Subject: [PATCH 070/117] add option to show local only or remote only or both --- lib/git-shell-out-strategy.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 5ea5d0ae56d..38f57924806 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -934,8 +934,13 @@ export default class GitShellOutStrategy { }); } - async getBranchesWithCommit(sha, {remotesOnly} = {}) { - const args = ['branch', ...(remotesOnly ? ['--remotes'] : []), '--format=%(refname:short)', '--contains', sha]; + async getBranchesWithCommit(sha, option = {}) { + const args = ['branch', '--format=%(refname:short)', '--contains', sha]; + if (option.showLocal && option.showRemote) { + args.splice(1, 0, '--all'); + } else if (option.showRemote) { + args.splice(1, 0, '--remotes'); + } return (await this.exec(args)).trim().split(LINE_ENDING_REGEX); } From adef37cab40952bf0fd4c101cc59a0d76f8623e7 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 18:10:38 +0100 Subject: [PATCH 071/117] don't use short ref --- 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 38f57924806..ea7bbad6063 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -935,7 +935,7 @@ export default class GitShellOutStrategy { } async getBranchesWithCommit(sha, option = {}) { - const args = ['branch', '--format=%(refname:short)', '--contains', sha]; + const args = ['branch', '--format=%(refname)', '--contains', sha]; if (option.showLocal && option.showRemote) { args.splice(1, 0, '--all'); } else if (option.showRemote) { From ff235a5a4c84f01ed1ad608d18602a13408ab191 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 18:11:24 +0100 Subject: [PATCH 072/117] `isCommitPushed` to find out if a commit exists on remote tracking branch --- lib/models/repository-states/present.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 6eb53323e60..5e323fc4e25 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -741,8 +741,10 @@ export default class Present extends State { }); } - isCommitPushed({sha}) { - return true; + async isCommitPushed(sha) { + const remoteBranchesWithCommit = await this.git().getBranchesWithCommit(sha, {showLocal: false, showRemote: true}); + const currentRemote = (await this.repository.getCurrentBranch()).getUpstream(); + return remoteBranchesWithCommit.includes(currentRemote.getFullRef()); } // Author information From 011a3be72d0f435e850f66ce5677f119feb52604 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 12:32:28 -0500 Subject: [PATCH 073/117] Enable hunk headers for CommitDetailItem --- lib/views/multi-file-patch-view.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 61414c30a3b..5d1407e4963 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -455,9 +455,6 @@ export default class MultiFilePatchView extends React.Component { } renderHunkHeaders(filePatch) { - if (this.props.itemType === CommitDetailItem) { - return null; - } const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; const selectedHunks = new Set( Array.from(this.props.selectedRows, row => this.props.multiFilePatch.getHunkAt(row)), From c54195aa4fe20ecec4ac99d0c7f70bf5dc9cb4d8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 12:36:01 -0500 Subject: [PATCH 074/117] Let that CSS actually target the avatar element --- lib/views/commit-detail-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 9438c075c2b..cdf82b45ff6 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -117,7 +117,7 @@ export default class CommitDetailView extends React.Component { } return ( - + {authorEmails.map(this.renderAuthor)} ); From fb2ec268cd1f8f140b4739ddc3651dc2f4dd3af5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 12:58:27 -0500 Subject: [PATCH 075/117] Fussing with button styles since I moved it --- styles/commit-detail.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/commit-detail.less b/styles/commit-detail.less index e6cab0bf147..a4d27f89ec1 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -47,8 +47,8 @@ &-moreButton { border: none; - margin-left: @default-padding/1.5; - padding: 0em .2em; + margin-bottom: @default-padding/1.5; + padding: 0em .4em; color: @text-color-subtle; font-style: italic; font-size: .8em; From 312eccc313db78682535566eeb44082c762f8396 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 14:44:45 -0500 Subject: [PATCH 076/117] Move the "Show More" button back to the header. Move the meta information up there, too. --- lib/views/commit-detail-view.js | 8 +++++--- styles/commit-detail.less | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index cdf82b45ff6..3d513cd6716 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -36,9 +36,10 @@ export default class CommitDetailView extends React.Component {
    -

    {emojify(commit.getMessageSubject())}

    - {this.renderCommitMessageBody()} - {this.renderShowMoreButton()} +

    + {emojify(commit.getMessageSubject())} + {this.renderShowMoreButton()} +

    {/* TODO fix image src */} {this.renderAuthors()} @@ -47,6 +48,7 @@ export default class CommitDetailView extends React.Component { {this.renderDotComLink()}
    + {this.renderCommitMessageBody()}
    Date: Thu, 29 Nov 2018 20:50:12 +0100 Subject: [PATCH 077/117] still show sha when it cannot be linked --- lib/views/commit-detail-view.js | 16 ++++++++-------- styles/commit-detail.less | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 3d513cd6716..e3e8b6ad4f0 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -46,7 +46,9 @@ export default class CommitDetailView extends React.Component { {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} - {this.renderDotComLink()} +
    + {this.renderDotComLink()} +
    {this.renderCommitMessageBody()}
    @@ -91,15 +93,13 @@ export default class CommitDetailView extends React.Component { if (remote && remote.isGithubRepo() && this.props.isCommitPushed) { const repoUrl = `https://www.github.com/${this.props.currentRemote.getOwner()}/${this.props.currentRemote.getRepo()}`; return ( - + + {sha} + ); } else { - return null; + return ({sha}); } } diff --git a/styles/commit-detail.less b/styles/commit-detail.less index e6cab0bf147..b349b8e2966 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -47,8 +47,8 @@ &-moreButton { border: none; - margin-left: @default-padding/1.5; - padding: 0em .2em; + margin-bottom: @default-padding/1.5; + padding: 0em .4em; color: @text-color-subtle; font-style: italic; font-size: .8em; @@ -78,12 +78,12 @@ flex: 0 0 7ch; // Limit to 7 characters margin-left: @default-padding*2; line-height: @avatar-dimensions; - color: @text-color-info; + color: @text-color-subtle; font-family: var(--editor-font-family); white-space: nowrap; overflow: hidden; a { - color: inherit; + color: @text-color-info; } } } From aedb0761f84ee22e8d3c7a6dd8e2fdc418d2db61 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 15:20:36 -0500 Subject: [PATCH 078/117] Turn that back to a left margin :art: --- styles/commit-detail.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/commit-detail.less b/styles/commit-detail.less index b349b8e2966..a6d5584545f 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -47,7 +47,7 @@ &-moreButton { border: none; - margin-bottom: @default-padding/1.5; + margin-left: @default-padding/1.5; padding: 0em .4em; color: @text-color-subtle; font-style: italic; From d007a3492f0c99b7052cdcabcb182924c5770f97 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 15:21:48 -0500 Subject: [PATCH 079/117] Drop LONG_MESSAGE_THRESHOLD to 500 and prepare newline counting --- lib/models/commit.js | 45 +++++++++------- test/models/commit.test.js | 104 +++++++++++++++---------------------- 2 files changed, 67 insertions(+), 82 deletions(-) diff --git a/lib/models/commit.js b/lib/models/commit.js index 3e7e65b3d41..99cccfa5317 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -1,9 +1,11 @@ const UNBORN = Symbol('unborn'); -export default class Commit { - static LONG_MESSAGE_THRESHOLD = 1000; +// Truncation elipsis styles +const WORD_ELIPSES = '...'; +const PARAGRAPH_ELIPSES = '\n\n...'; - static BOUNDARY_SEARCH_THRESHOLD = 100; +export default class Commit { + static LONG_MESSAGE_THRESHOLD = 500; static createUnborn() { return new Commit({unbornRef: UNBORN}); @@ -64,29 +66,32 @@ export default class Commit { return this.getMessageBody(); } - const {LONG_MESSAGE_THRESHOLD, BOUNDARY_SEARCH_THRESHOLD} = this.constructor; - let elipses = '...'; - let lastParagraphIndex = Infinity; - let lastWordIndex = Infinity; - const lastSubwordIndex = BOUNDARY_SEARCH_THRESHOLD - elipses.length; + const {LONG_MESSAGE_THRESHOLD} = this.constructor; - const baseIndex = LONG_MESSAGE_THRESHOLD - BOUNDARY_SEARCH_THRESHOLD; - const boundarySearch = this.getMessageBody().substring(baseIndex, LONG_MESSAGE_THRESHOLD); + let lastParagraphCutoff = null; + let lastWordCutoff = null; - const boundaryRx = /\r?\n\r?\n|\s+/g; + const searchText = this.getMessageBody().substring(0, LONG_MESSAGE_THRESHOLD); + const boundaryRx = /\s+/g; let result; - while ((result = boundaryRx.exec(boundarySearch)) !== null) { - if (/\r?\n\r?\n/.test(result[0])) { - // Paragraph boundary. Omit the elipses - lastParagraphIndex = result.index; - elipses = ''; - } else if (result.index <= BOUNDARY_SEARCH_THRESHOLD - elipses.length) { - // Word boundary. Only count if we have room for the elipses under the cutoff. - lastWordIndex = result.index; + while ((result = boundaryRx.exec(searchText)) !== null) { + const newlineCount = (result[0].match(/\r?\n/g) || []).length; + if (newlineCount < 2 && result.index <= LONG_MESSAGE_THRESHOLD - WORD_ELIPSES.length) { + lastWordCutoff = result.index; + } else if (result.index < LONG_MESSAGE_THRESHOLD - PARAGRAPH_ELIPSES.length) { + lastParagraphCutoff = result.index; } } - const cutoffIndex = baseIndex + Math.min(lastParagraphIndex, lastWordIndex, lastSubwordIndex); + let elipses = WORD_ELIPSES; + let cutoffIndex = LONG_MESSAGE_THRESHOLD - WORD_ELIPSES.length; + if (lastParagraphCutoff !== null) { + elipses = PARAGRAPH_ELIPSES; + cutoffIndex = lastParagraphCutoff; + } else if (lastWordCutoff !== null) { + cutoffIndex = lastWordCutoff; + } + return this.getMessageBody().substring(0, cutoffIndex) + elipses; } diff --git a/test/models/commit.test.js b/test/models/commit.test.js index 9e3e7664eab..ee9f28f43e7 100644 --- a/test/models/commit.test.js +++ b/test/models/commit.test.js @@ -45,108 +45,88 @@ describe('Commit', function() { assert.strictEqual(commit.abbreviatedBody(), 'short'); }); - it('truncates the message body at the nearest paragraph boundary before the cutoff if one is nearby', function() { - // The | is at the 1000-character mark. + it('truncates the message body at the last paragraph boundary before the cutoff if one is present', function() { const body = dedent` Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, - essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam - tantas nullam corrumpit ad, in oratio luptatum eleifend vim. + essent malorum persius ne mei. - Ea salutatus contentiones eos. Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere - urbanitas, usu ut aperiri mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, - scripta iudicabit ne nam, in duis clita commodo sit. + Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. Eam in veniam facete + volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri mediocritatem, alia + molestie urbanitas cu qui. - Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et eum - voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus + Velit antiopam erroribus no eu|m, scripta iudicabit ne nam, in duis clita commodo + sit. Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et + eum voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam reprehendunt et mea. Ea eius omnes voluptua sit. - - No cum illud verear efficiantur. Id altera imperdiet nec. Noster aud|iam accusamus mei at, no zril libris nemore - duo, ius ne rebum doctus fuisset. Legimus epicurei in sit, esse purto suscipit eu qui, oporteat deserunt - delicatissimi sea in. Est id putent accusata convenire, no tibique molestie accommodare quo, cu est fuisset - offendit evertitur. `; const commit = commitBuilder().messageBody(body).build(); assert.strictEqual(commit.abbreviatedBody(), dedent` Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, - essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam - tantas nullam corrumpit ad, in oratio luptatum eleifend vim. + essent malorum persius ne mei. - Ea salutatus contentiones eos. Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere - urbanitas, usu ut aperiri mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, - scripta iudicabit ne nam, in duis clita commodo sit. + Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. Eam in veniam facete + volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri mediocritatem, alia + molestie urbanitas cu qui. - Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et eum - voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus - tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam - reprehendunt et mea. Ea eius omnes voluptua sit. + ... `); }); - it('truncates the message body at the nearest word boundary before the cutoff if one is nearby', function() { - // The | is at the 1000-character mark. + it('truncates the message body at the nearest word boundary before the cutoff if one is present', function() { + // The | is at the 500-character mark. const body = dedent` - Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. - - Mazim alterum sea ea, essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore - albucius te vis, eam tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. - Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri - mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, scripta iudicabit ne nam, in - duis clita commodo sit. Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat - comprehensam ut his, et eum voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei - liber putant. Ad doctus tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent - philosophia et vix. Nusquam reprehendunt et mea. Ea eius omnes voluptua sit. No cum illud verear efficiantur. Id - altera imperdiet nec. Noster audia|m accusamus mei at, no zril libris nemore duo, ius ne rebum doctus fuisset. - Legimus epicurei in sit, esse purto suscipit eu qui, oporteat deserunt delicatissimi sea in. Est id putent - accusata convenire, no tibique molestie accommodare quo, cu est fuisset offendit evertitur. + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, + essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. Eam in veniam facete + volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri mediocritatem, alia + molestie urbanitas cu qui. Velit antiopam erroribus no eum,| scripta iudicabit ne nam, in duis clita commodo + sit. Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat comprehensam ut his, et + eum voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei liber putant. Ad doctus + tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent philosophia et vix. Nusquam + reprehendunt et mea. Ea eius omnes voluptua sit. No cum illud verear efficiantur. Id altera imperdiet nec. + Noster audia|m accusamus mei at, no zril libris nemore duo, ius ne rebum doctus fuisset. Legimus epicurei in + sit, esse purto suscipit eu qui, oporteat deserunt delicatissimi sea in. Est id putent accusata convenire, no + tibique molestie accommodare quo, cu est fuisset offendit evertitur. `; const commit = commitBuilder().messageBody(body).build(); assert.strictEqual(commit.abbreviatedBody(), dedent` - Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. - - Mazim alterum sea ea, essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore - albucius te vis, eam tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. - Eam in veniam facete volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri - mediocritatem, alia molestie urbanitas cu qui. Velit antiopam erroribus no eum, scripta iudicabit ne nam, in - duis clita commodo sit. Assum sensibus oportere te vel, vis semper evertitur definiebas in. Tamquam feugiat - comprehensam ut his, et eum voluptua ullamcorper, ex mei debitis inciderint. Sit discere pertinax te, an mei - liber putant. Ad doctus tractatos ius, duo ad civibus alienum, nominati voluptaria sed an. Libris essent - philosophia et vix. Nusquam reprehendunt et mea. Ea eius omnes voluptua sit. No cum illud verear efficiantur. Id - altera imperdiet nec. Noster... + Lorem ipsum dolor sit amet, et his justo deleniti, omnium fastidii adversarium at has. Mazim alterum sea ea, + essent malorum persius ne mei. Nam ea tempor qualisque, modus doming te has. Affert dolore albucius te vis, eam + tantas nullam corrumpit ad, in oratio luptatum eleifend vim. Ea salutatus contentiones eos. Eam in veniam facete + volutpat, solum appetere adversarium ut quo. Vel cu appetere urbanitas, usu ut aperiri mediocritatem, alia + molestie urbanitas cu qui. Velit antiopam erroribus no... `); }); it('truncates the message body at the character cutoff if no word or paragraph boundaries can be found', function() { - // The | is at the 1000-character mark. - const body = 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.\n\n' + - 'Mazim alterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' + + // The | is at the 500-character mark. + const body = 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.' + + 'Mazimalterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' + 'albuciustevis,eamtantasnullamcorrumpitad,inoratioluptatumeleifendvim.Easalutatuscontentioneseos.' + 'Eaminveniamfacetevolutpat,solumappetereadversariumutquo.Velcuappetereurbanitas,usuutaperiri' + 'mediocritatem,aliamolestieurbanitascuqui.Velitantiopamerroribusnoeum,scriptaiudicabitnenam,in' + - 'duisclitacommodosit.Assumsensibusoporteretevel,vissemperevertiturdefiniebasin.Tamquamfeugiat' + + 'duisclitacommodosit.Assumsensibusoporteretevel,vissem|perevertiturdefiniebasin.Tamquamfeugiat' + 'comprehensamuthis,eteumvoluptuaullamcorper,exmeidebitisinciderint.Sitdiscerepertinaxte,anmei' + 'liberputant.Addoctustractatosius,duoadcivibusalienum,nominativoluptariasedan.Librisessent' + 'philosophiaetvix.Nusquamreprehenduntetmea.Eaeiusomnesvoluptuasit.Nocumilludverearefficiantur.Id' + 'alteraimperdietnec.Nosteraudiamaccusamusmeiat,nozrillibrisnemoreduo,iusnerebumdoctusfuisset.' + 'Legimusepicureiinsit,essepurtosuscipiteuqui,oporteatdeseruntdelicatissimiseain.Estidputent' + - '|accusataconvenire,notibiquemolestieaccommodarequo,cuestfuissetoffenditevertitur.'; + 'accusataconvenire,notibiquemolestieaccommodarequo,cuestfuissetoffenditevertitur.'; const commit = commitBuilder().messageBody(body).build(); assert.strictEqual( commit.abbreviatedBody(), - 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.\n\n' + - 'Mazim alterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' + + 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.' + + 'Mazimalterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' + 'albuciustevis,eamtantasnullamcorrumpitad,inoratioluptatumeleifendvim.Easalutatuscontentioneseos.' + 'Eaminveniamfacetevolutpat,solumappetereadversariumutquo.Velcuappetereurbanitas,usuutaperiri' + 'mediocritatem,aliamolestieurbanitascuqui.Velitantiopamerroribusnoeum,scriptaiudicabitnenam,in' + - 'duisclitacommodosit.Assumsensibusoporteretevel,vissemperevertiturdefiniebasin.Tamquamfeugiat' + - 'comprehensamuthis,eteumvoluptuaullamcorper,exmeidebitisinciderint.Sitdiscerepertinaxte,anmei' + - 'liberputant.Addoctustractatosius,duoadcivibusalienum,nominativoluptariasedan.Librisessent' + - 'philosophiaetvix.Nusquamreprehenduntetmea.Eaeiusomnesvoluptuasit.Nocumilludverearefficiantur.Id' + - 'alteraimperdietnec.Nosteraudiamaccusamusmeiat,nozrillibrisnemoreduo,iusnerebumdoctusfuisset.' + - 'Legimusepicureiinsit,essepurtosuscipiteuqui,oporteatdeseruntdelicatissimiseain.Estidput...', + 'duisclitacommodosit.Assumsensibusoporteretevel,vis...', ); }); }); From 682503cc3fe0687b21c91df89299f47b2b0bec93 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 15:33:42 -0500 Subject: [PATCH 080/117] Armor the abbreviation logic against messages with many short lines --- lib/models/commit.js | 29 ++++++++++++++++++++++++++--- test/models/commit.test.js | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/models/commit.js b/lib/models/commit.js index 99cccfa5317..4f69679c19d 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -2,11 +2,14 @@ const UNBORN = Symbol('unborn'); // Truncation elipsis styles const WORD_ELIPSES = '...'; +const NEWLINE_ELIPSES = '\n...'; const PARAGRAPH_ELIPSES = '\n\n...'; export default class Commit { static LONG_MESSAGE_THRESHOLD = 500; + static NEWLINE_THRESHOLD = 9; + static createUnborn() { return new Commit({unbornRef: UNBORN}); } @@ -47,7 +50,15 @@ export default class Commit { } isBodyLong() { - return this.getMessageBody().length > this.constructor.LONG_MESSAGE_THRESHOLD; + if (this.getMessageBody().length > this.constructor.LONG_MESSAGE_THRESHOLD) { + return true; + } + + if ((this.getMessageBody().match(/\r?\n/g) || []).length > this.constructor.NEWLINE_THRESHOLD) { + return true; + } + + return false; } getFullMessage() { @@ -66,16 +77,25 @@ export default class Commit { return this.getMessageBody(); } - const {LONG_MESSAGE_THRESHOLD} = this.constructor; + const {LONG_MESSAGE_THRESHOLD, NEWLINE_THRESHOLD} = this.constructor; + let lastNewlineCutoff = null; let lastParagraphCutoff = null; let lastWordCutoff = null; const searchText = this.getMessageBody().substring(0, LONG_MESSAGE_THRESHOLD); const boundaryRx = /\s+/g; let result; + let lineCount = 0; while ((result = boundaryRx.exec(searchText)) !== null) { const newlineCount = (result[0].match(/\r?\n/g) || []).length; + + lineCount += newlineCount; + if (lineCount > NEWLINE_THRESHOLD) { + lastNewlineCutoff = result.index; + break; + } + if (newlineCount < 2 && result.index <= LONG_MESSAGE_THRESHOLD - WORD_ELIPSES.length) { lastWordCutoff = result.index; } else if (result.index < LONG_MESSAGE_THRESHOLD - PARAGRAPH_ELIPSES.length) { @@ -85,7 +105,10 @@ export default class Commit { let elipses = WORD_ELIPSES; let cutoffIndex = LONG_MESSAGE_THRESHOLD - WORD_ELIPSES.length; - if (lastParagraphCutoff !== null) { + if (lastNewlineCutoff !== null) { + elipses = NEWLINE_ELIPSES; + cutoffIndex = lastNewlineCutoff; + } else if (lastParagraphCutoff !== null) { elipses = PARAGRAPH_ELIPSES; cutoffIndex = lastParagraphCutoff; } else if (lastWordCutoff !== null) { diff --git a/test/models/commit.test.js b/test/models/commit.test.js index ee9f28f43e7..4b4d6aa08d8 100644 --- a/test/models/commit.test.js +++ b/test/models/commit.test.js @@ -34,6 +34,15 @@ describe('Commit', function() { assert.isTrue(commit.isBodyLong()); }); + it('returns true if the commit message body contains too many newlines', function() { + let messageBody = 'a\n'; + for (let i = 0; i < 50; i++) { + messageBody += 'a\n'; + } + const commit = commitBuilder().messageBody(messageBody).build(); + assert.isTrue(commit.isBodyLong()); + }); + it('returns false for a null commit', function() { assert.isFalse(nullCommit.isBodyLong()); }); @@ -129,5 +138,14 @@ describe('Commit', function() { 'duisclitacommodosit.Assumsensibusoporteretevel,vis...', ); }); + + it('truncates the message body when it contains too many newlines', function() { + let messageBody = ''; + for (let i = 0; i < 50; i++) { + messageBody += `${i}\n`; + } + const commit = commitBuilder().messageBody(messageBody).build(); + assert.strictEqual(commit.abbreviatedBody(), '0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n...'); + }); }); }); From aff05e5c50e2d8a73e8a2e074a4ea1c12bc8e36b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 29 Nov 2018 15:46:42 -0500 Subject: [PATCH 081/117] Allow the CommitDetailView header to focus and use native bindings --- lib/views/commit-detail-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index e3e8b6ad4f0..2684c7c28d5 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -34,7 +34,7 @@ export default class CommitDetailView extends React.Component { return (
    -
    +

    {emojify(commit.getMessageSubject())} From 5b0c75a26577b62408c52978660c1ce2fb44f164 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 29 Nov 2018 13:14:53 -0800 Subject: [PATCH 082/117] set a max-height on the commit message body text. We want the content beneath to remain visible / scrollable. --- styles/commit-detail.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/styles/commit-detail.less b/styles/commit-detail.less index a6d5584545f..5044469874e 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -69,6 +69,9 @@ word-break: break-word; white-space: pre-wrap; background-color: transparent; + // in the case of loonnng commit message bodies, we want to cap the height so that + // the content beneath will remain visible / scrollable. + max-height: 55vh; &:empty { display: none; } From 5fa5e65775272af634cdfb4002ba471e701c6e48 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 21:31:39 +0100 Subject: [PATCH 083/117] update function name in state --- lib/models/repository-states/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 82c443a255e..747b7b3ac54 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -306,7 +306,7 @@ export default class State { return Promise.resolve([]); } - isCommitPushed({sha}) { + isCommitPushed(sha) { return false; } From dfa6c6a619487add5da72447e893f8bb4f1865c5 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 21:32:23 +0100 Subject: [PATCH 084/117] add isCommitPushed to default returns a null object test --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 2eaac205e5f..99a3ec27467 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -64,7 +64,7 @@ describe('Repository', function() { for (const method of [ 'isLoadingGuess', 'isAbsentGuess', 'isAbsent', 'isLoading', 'isEmpty', 'isPresent', 'isTooLarge', 'isUndetermined', 'showGitTabInit', 'showGitTabInitInProgress', 'showGitTabLoading', 'showStatusBarTiles', - 'hasDiscardHistory', 'isMerging', 'isRebasing', + 'hasDiscardHistory', 'isMerging', 'isRebasing', 'isCommitPushed', ]) { assert.isFalse(await repository[method]()); } From b7ef5ed750cb44d5a0ccff1907e3a2de5a423aee Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 29 Nov 2018 22:21:15 +0100 Subject: [PATCH 085/117] after undoing a commit, close corresponding commit item pane, if opened. --- lib/controllers/git-tab-controller.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 57087c7170f..f3322f4c797 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -11,6 +11,7 @@ import { CommitPropType, BranchPropType, FilePatchItemPropType, MergeConflictItemPropType, RefHolderPropType, } from '../prop-types'; import {autobind} from '../helpers'; +import CommitDetailItem from '../items/commit-detail-item'; export default class GitTabController extends React.Component { static focus = { @@ -277,6 +278,16 @@ export default class GitTabController extends React.Component { new Author(author.email, author.name)); this.updateSelectedCoAuthors(coAuthors); + + // close corresponding commit item pane, if opened + const uri = CommitDetailItem.buildURI(this.props.repository.getWorkingDirectoryPath(), lastCommit.getSha()); + const pane = this.props.workspace.paneForURI(uri); + if (pane) { + const item = pane.itemForURI(uri); + if (item) { + await pane.destroyItem(item); + } + } return null; } From 9341c95edcca0eebb6adc05c45ab455c6a2d43e0 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 30 Nov 2018 13:03:13 +0900 Subject: [PATCH 086/117] Move "show more" button --- lib/views/commit-detail-view.js | 2 +- styles/commit-detail.less | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 2684c7c28d5..e76b70ec22c 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -38,7 +38,6 @@ export default class CommitDetailView extends React.Component {

    {emojify(commit.getMessageSubject())} - {this.renderShowMoreButton()}

    {/* TODO fix image src */} @@ -50,6 +49,7 @@ export default class CommitDetailView extends React.Component { {this.renderDotComLink()}
    + {this.renderShowMoreButton()} {this.renderCommitMessageBody()}

    diff --git a/styles/commit-detail.less b/styles/commit-detail.less index 5044469874e..9f9bcc47620 100644 --- a/styles/commit-detail.less +++ b/styles/commit-detail.less @@ -16,6 +16,7 @@ &-commit { padding: @default-padding*2; + padding-bottom: 0; } &-title { @@ -35,7 +36,7 @@ &-meta { display: flex; align-items: center; - margin-top: @default-padding/2; + margin: @default-padding/2 0 @default-padding*2 0; } &-metaText { @@ -46,12 +47,12 @@ } &-moreButton { - border: none; - margin-left: @default-padding/1.5; + position: absolute; + left: 50%; + transform: translate(-50%, -50%); padding: 0em .4em; color: @text-color-subtle; font-style: italic; - font-size: .8em; border: 1px solid @base-border-color; border-radius: @component-border-radius; background-color: @button-background-color; @@ -62,12 +63,13 @@ } &-moreText { - padding: @default-padding/2 0 @default-padding 0; + padding: @default-padding*2 0; font-size: inherit; font-family: var(--editor-font-family); word-wrap: initial; word-break: break-word; white-space: pre-wrap; + border-top: 1px solid @base-border-color; background-color: transparent; // in the case of loonnng commit message bodies, we want to cap the height so that // the content beneath will remain visible / scrollable. From 99ba3bc4cc2c0e512d9116e46b823b271416d035 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 29 Nov 2018 21:00:40 -0800 Subject: [PATCH 087/117] [wip] keyboard navigability in RecentCommitsView ...I feel like I'm doing this wrong. Feel free to revert and start over. --- keymaps/git.cson | 5 ++++ lib/controllers/recent-commits-controller.js | 2 ++ lib/views/git-tab-view.js | 7 +++++ lib/views/recent-commits-view.js | 29 ++++++++++++++++++++ test/views/git-tab-view.test.js | 9 ++++++ 5 files changed, 52 insertions(+) diff --git a/keymaps/git.cson b/keymaps/git.cson index 038e4d942c6..f6236b9a07c 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -32,6 +32,11 @@ 'tab': 'core:focus-next' 'shift-tab': 'core:focus-previous' +'.github-RecentCommitsView': + 'up': 'github:recent-commit-up' + 'down': 'github:recent-commit-down' + 'enter': 'github:open-recent-commit' + '.github-StagingView.unstaged-changes-focused': 'cmd-backspace': 'github:discard-changes-in-selected-files' 'ctrl-backspace': 'github:discard-changes-in-selected-files' diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index 2e72da64f97..e97b079192c 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -15,6 +15,7 @@ export default class RecentCommitsController extends React.Component { undoLastCommit: PropTypes.func.isRequired, workspace: PropTypes.object.isRequired, repository: PropTypes.object.isRequired, + commandRegistry: PropTypes.object.isRequired, } constructor(props, context) { @@ -54,6 +55,7 @@ export default class RecentCommitsController extends React.Component { undoLastCommit={this.props.undoLastCommit} openCommit={this.openCommit} selectedCommitSha={this.state.selectedCommitSha} + commandRegistry={this.props.commandRegistry} /> ); } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index fb7339869db..322cc3ad97d 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -7,6 +7,7 @@ import StagingView from './staging-view'; import GitLogo from './git-logo'; import CommitController from '../controllers/commit-controller'; import RecentCommitsController from '../controllers/recent-commits-controller'; +import RecentCommitsView from '../views/recent-commits-view'; import RefHolder from '../models/ref-holder'; import {isValidWorkdir, autobind} from '../helpers'; import {AuthorPropType, UserStorePropType, RefHolderPropType} from '../prop-types'; @@ -16,6 +17,7 @@ export default class GitTabView extends React.Component { ...StagingView.focus, ...CommitController.focus, ...RecentCommitsController.focus, + ...RecentCommitsView.focus, }; static propTypes = { @@ -193,6 +195,7 @@ export default class GitTabView extends React.Component { updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} /> view.setFocus(focus)).getOr(false); + } + + rememberFocus(event) { + return this.refRecentCommits.map(view => view.rememberFocus(event)).getOr(null); + } + + selectNextCommit() { + // okay, we should probably move the state of the selected commit into this component + // instead of using the sha, so we can more easily move to next / previous. + } + render() { return (
    + + + {this.renderCommits()}
    ); diff --git a/test/views/git-tab-view.test.js b/test/views/git-tab-view.test.js index c21f152e91c..5164c65ecb3 100644 --- a/test/views/git-tab-view.test.js +++ b/test/views/git-tab-view.test.js @@ -234,4 +234,13 @@ describe('GitTabView', function() { assert.isTrue(setFocus.called); assert.isTrue(setFocus.lastCall.returnValue); }); + + it('imperatively focuses the recent commits view', async function() { + const wrapper = mount(await buildApp()); + + const setFocus = sinon.spy(wrapper.find('RecentCommitsView').instance(), 'setFocus'); + wrapper.instance().focusAndSelectRecentCommit(); + assert.isTrue(setFocus.called); + assert.isTrue(setFocus.lastCall.returnValue); + }); }); From 2bed5f1db9d939f6fde9553157796506a7b20971 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Nov 2018 17:06:11 -0800 Subject: [PATCH 088/117] Display co-author information --- lib/views/commit-detail-view.js | 13 +++++++--- test/views/commit-detail-view.test.js | 35 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index e76b70ec22c..40a5280618c 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -43,7 +43,7 @@ export default class CommitDetailView extends React.Component { {/* TODO fix image src */} {this.renderAuthors()} - {commit.getAuthorEmail()} committed {this.humanizeTimeSince(commit.getAuthorDate())} + {this.getAuthorInfo()} committed {this.humanizeTimeSince(commit.getAuthorDate())}
    {this.renderDotComLink()} @@ -104,8 +104,15 @@ export default class CommitDetailView extends React.Component { } getAuthorInfo() { - const coAuthorCount = this.props.commit.getCoAuthors().length; - return coAuthorCount ? this.props.commit.getAuthorEmail() : `${coAuthorCount + 1} people`; + const commit = this.props.commit; + const coAuthorCount = commit.getCoAuthors().length; + if (coAuthorCount === 0) { + return commit.getAuthorEmail(); + } else if (coAuthorCount === 1) { + return `${commit.getAuthorEmail()} and ${commit.getCoAuthors()[0].email}`; + } else { + return `${commit.getAuthorEmail()} and ${coAuthorCount} others`; + } } renderAuthor(email) { diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js index acca9a093d8..e56c35a0427 100644 --- a/test/views/commit-detail-view.test.js +++ b/test/views/commit-detail-view.test.js @@ -93,6 +93,41 @@ describe('CommitDetailView', function() { ); }); + describe('getAuthorInfo', function() { + describe('when there are no co-authors', function() { + it('returns only the author', function() { + const commit = commitBuilder() + .authorEmail('blaze@it.com') + .build(); + const wrapper = shallow(buildApp({commit})); + assert.strictEqual(wrapper.instance().getAuthorInfo(), 'blaze@it.com'); + }); + }); + + describe('when there is one co-author', function() { + it('returns author and the co-author', function() { + const commit = commitBuilder() + .authorEmail('blaze@it.com') + .addCoAuthor('two', 'two@coauthor.com') + .build(); + const wrapper = shallow(buildApp({commit})); + assert.strictEqual(wrapper.instance().getAuthorInfo(), 'blaze@it.com and two@coauthor.com'); + }); + }); + + describe('when there is more than one co-author', function() { + it('returns the author and number of co-authors', function() { + const commit = commitBuilder() + .authorEmail('blaze@it.com') + .addCoAuthor('two', 'two@coauthor.com') + .addCoAuthor('three', 'three@coauthor.com') + .build(); + const wrapper = shallow(buildApp({commit})); + assert.strictEqual(wrapper.instance().getAuthorInfo(), 'blaze@it.com and 2 others'); + }); + }); + }); + describe('commit message collapsibility', function() { let wrapper, shortMessage, longMessage; From d5efe90bd53e024e4deccf0c8ddf549a4ab7a7ec Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Nov 2018 17:57:14 -0800 Subject: [PATCH 089/117] Add test for RecentCommitsController#openCommit, including event recording --- lib/controllers/recent-commits-controller.js | 2 +- .../recent-commits-controller.test.js | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index e97b079192c..a257cab1fce 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -64,7 +64,7 @@ export default class RecentCommitsController extends React.Component { const workdir = this.props.repository.getWorkingDirectoryPath(); const uri = CommitDetailItem.buildURI(workdir, sha); this.props.workspace.open(uri).then(() => { - addEvent('open-commit-in-pane', {package: 'github', from: 'recent commit'}); + addEvent('open-commit-in-pane', {package: 'github', from: this.constructor.name}); }); } } diff --git a/test/controllers/recent-commits-controller.test.js b/test/controllers/recent-commits-controller.test.js index 66151879b14..f69c33ae172 100644 --- a/test/controllers/recent-commits-controller.test.js +++ b/test/controllers/recent-commits-controller.test.js @@ -3,6 +3,8 @@ import {shallow} from 'enzyme'; import RecentCommitsController from '../../lib/controllers/recent-commits-controller'; import Commit from '../../lib/models/commit'; +import {cloneRepository, buildRepository} from '../helpers'; +import * as reporterProxy from '../../lib/reporter-proxy'; describe('RecentCommitsController', function() { let app; @@ -23,4 +25,46 @@ describe('RecentCommitsController', function() { const wrapper = shallow(app); assert.isTrue(wrapper.find('RecentCommitsView').prop('isLoading')); }); + + describe('openCommit({sha})', function() { + let atomEnv, workdirPath, repository; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + workdirPath = await cloneRepository(); + repository = await buildRepository(workdirPath); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + it('opens a commit detail item', function() { + sinon.stub(atomEnv.workspace, 'open').resolves(); + + const sha = 'asdf1234'; + const commits = [new Commit({sha})]; + app = React.cloneElement(app, {commits, workspace: atomEnv.workspace, repository}); + const wrapper = shallow(app); + wrapper.instance().openCommit({sha: 'asdf1234'}); + + assert.isTrue(atomEnv.workspace.open.calledWith( + `atom-github://commit-detail?workdir=${encodeURIComponent(workdirPath)}` + + `&sha=${encodeURIComponent(sha)}`, + )); + }); + + it('records an event', async function() { + sinon.stub(atomEnv.workspace, 'open').resolves(); + sinon.stub(reporterProxy, 'addEvent'); + + const sha = 'asdf1234'; + const commits = [new Commit({sha})]; + app = React.cloneElement(app, {commits, workspace: atomEnv.workspace, repository}); + const wrapper = shallow(app); + + await wrapper.instance().openCommit({sha: 'asdf1234'}); + assert.isTrue(reporterProxy.addEvent.calledWith('open-commit-in-pane', {package: 'github', from: RecentCommitsController.name})); + }); + }); }); From 8b3c56894fe3ed11e55cad0b97559bb2e5139233 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Nov 2018 22:33:00 -0800 Subject: [PATCH 090/117] Add tests for OpenCommitDialog --- lib/controllers/root-controller.js | 2 +- test/controllers/root-controller.test.js | 57 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index c57c7a9d4f7..ab6f9f588f1 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -597,7 +597,7 @@ export default class RootController extends React.Component { const uri = CommitDetailItem.buildURI(workdir, sha); this.setState({openCommitDialogActive: false}); this.props.workspace.open(uri).then(() => { - addEvent('open-commit-in-pane', {package: 'github', from: 'dialog'}); + addEvent('open-commit-in-pane', {package: 'github', from: OpenCommitDialog.name}); }); } diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 7edebaab67b..8c7a4168dcd 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -17,9 +17,11 @@ import GitHubTabItem from '../../lib/items/github-tab-item'; import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; import IssueishDetailItem from '../../lib/items/issueish-detail-item'; import CommitPreviewItem from '../../lib/items/commit-preview-item'; +import CommitDetailItem from '../../lib/items/commit-detail-item'; import * as reporterProxy from '../../lib/reporter-proxy'; import RootController from '../../lib/controllers/root-controller'; +import OpenCommitDialog from '../../lib/views/open-commit-dialog'; describe('RootController', function() { let atomEnv, app; @@ -323,6 +325,61 @@ describe('RootController', function() { }); }); + describe('github:open-commit', function() { + let workdirPath, wrapper, openCommitDetails, resolveOpenCommit; + + beforeEach(async function() { + openCommitDetails = sinon.stub(atomEnv.workspace, 'open').returns(new Promise(resolve => { + resolveOpenCommit = resolve; + })); + + workdirPath = await cloneRepository('multiple-commits'); + const repository = await buildRepository(workdirPath); + + app = React.cloneElement(app, {repository}); + wrapper = shallow(app); + }); + + it('renders the modal open-commit panel', function() { + wrapper.instance().showOpenCommitDialog(); + wrapper.update(); + + assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('OpenCommitDialog'), 1); + }); + + it('triggers the open callback on accept and fires `open-commit-in-pane` event', async function() { + sinon.stub(reporterProxy, 'addEvent'); + wrapper.instance().showOpenCommitDialog(); + wrapper.update(); + + const dialog = wrapper.find('OpenCommitDialog'); + const sha = 'asdf1234'; + + const promise = dialog.prop('didAccept')({sha}); + resolveOpenCommit(); + await promise; + + const uri = CommitDetailItem.buildURI(workdirPath, sha); + + assert.isTrue(openCommitDetails.calledWith(uri)); + + await assert.isTrue(reporterProxy.addEvent.calledWith('open-commit-in-pane', {package: 'github', from: OpenCommitDialog.name})); + }); + + it('dismisses the open-commit panel on cancel', function() { + wrapper.instance().showOpenCommitDialog(); + wrapper.update(); + + const dialog = wrapper.find('OpenCommitDialog'); + dialog.prop('didCancel')(); + + wrapper.update(); + assert.lengthOf(wrapper.find('OpenCommitDialog'), 0); + assert.isFalse(openCommitDetails.called); + assert.isFalse(wrapper.state('openCommitDialogActive')); + }); + }); + describe('github:clone', function() { let wrapper, cloneRepositoryForProjectPath, resolveClone, rejectClone; From 994525dbd328b5865eb9445243c2c0d907a89826 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Nov 2018 22:42:24 -0800 Subject: [PATCH 091/117] Add tests for OpenIssuishDialog --- lib/controllers/root-controller.js | 1 + test/controllers/root-controller.test.js | 58 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index ab6f9f588f1..282150c71d9 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -582,6 +582,7 @@ export default class RootController extends React.Component { acceptOpenIssueish({repoOwner, repoName, issueishNumber}) { const uri = IssueishDetailItem.buildURI('https://api.github.com', repoOwner, repoName, issueishNumber); + console.warn(uri); this.setState({openIssueishDialogActive: false}); this.props.workspace.open(uri).then(() => { addEvent('open-issueish-in-pane', {package: 'github', from: 'dialog'}); diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 8c7a4168dcd..7769fd4fa5c 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -22,6 +22,7 @@ import * as reporterProxy from '../../lib/reporter-proxy'; import RootController from '../../lib/controllers/root-controller'; import OpenCommitDialog from '../../lib/views/open-commit-dialog'; +import OpenIssueishDialog from '../../lib/views/open-issueish-dialog'; describe('RootController', function() { let atomEnv, app; @@ -380,6 +381,63 @@ describe('RootController', function() { }); }); + describe('github:open-issue-or-pull-request', function() { + let workdirPath, wrapper, openIssueishDetails, resolveOpenIssueish; + + beforeEach(async function() { + openIssueishDetails = sinon.stub(atomEnv.workspace, 'open').returns(new Promise(resolve => { + resolveOpenIssueish = resolve; + })); + + workdirPath = await cloneRepository('multiple-commits'); + const repository = await buildRepository(workdirPath); + + app = React.cloneElement(app, {repository}); + wrapper = shallow(app); + }); + + it('renders the modal open-commit panel', function() { + wrapper.instance().showOpenIssueishDialog(); + wrapper.update(); + + assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('OpenIssueishDialog'), 1); + }); + + it('triggers the open callback on accept and fires `open-commit-in-pane` event', async function() { + sinon.stub(reporterProxy, 'addEvent'); + wrapper.instance().showOpenIssueishDialog(); + wrapper.update(); + + const dialog = wrapper.find('OpenIssueishDialog'); + const repoOwner = 'owner'; + const repoName = 'name'; + const issueishNumber = 1234; + + const promise = dialog.prop('didAccept')({repoOwner, repoName, issueishNumber}); + resolveOpenIssueish(); + await promise; + + const uri = IssueishDetailItem.buildURI('https://api.github.com', repoOwner, repoName, issueishNumber); + + assert.isTrue(openIssueishDetails.calledWith(uri)); + + await assert.isTrue(reporterProxy.addEvent.calledWith('open-issueish-in-pane', {package: 'github', from: 'dialog'})); + }); + + it('dismisses the open-commit panel on cancel', function() { + wrapper.instance().showOpenIssueishDialog(); + wrapper.update(); + + const dialog = wrapper.find('OpenIssueishDialog'); + dialog.prop('didCancel')(); + + wrapper.update(); + assert.lengthOf(wrapper.find('OpenIssueishDialog'), 0); + assert.isFalse(openIssueishDetails.called); + assert.isFalse(wrapper.state('openIssueishDialogActive')); + }); + }); + describe('github:clone', function() { let wrapper, cloneRepositoryForProjectPath, resolveClone, rejectClone; From 664b7a94df8a9615846aafcebefdd39e1235057e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Nov 2018 22:45:35 -0800 Subject: [PATCH 092/117] Open CommitDetailItem from RecentCommitView as pending item --- lib/controllers/recent-commits-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index a257cab1fce..45eeea47640 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -63,7 +63,7 @@ export default class RecentCommitsController extends React.Component { openCommit({sha}) { const workdir = this.props.repository.getWorkingDirectoryPath(); const uri = CommitDetailItem.buildURI(workdir, sha); - this.props.workspace.open(uri).then(() => { + this.props.workspace.open(uri, {pending: true}).then(() => { addEvent('open-commit-in-pane', {package: 'github', from: this.constructor.name}); }); } From 02034767f22d8b0a7a0e9772e168e9ce5c13cc70 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 30 Nov 2018 00:40:50 -0800 Subject: [PATCH 093/117] Open CommitDetailItem from PrCommitView --- lib/containers/issueish-detail-container.js | 2 ++ lib/controllers/issueish-detail-controller.js | 15 ++++++++++++++- lib/controllers/root-controller.js | 2 ++ lib/items/issueish-detail-item.js | 3 +++ lib/views/issueish-detail-view.js | 6 +++++- lib/views/pr-commit-view.js | 12 +++++++++++- lib/views/pr-commits-view.js | 2 ++ 7 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/containers/issueish-detail-container.js b/lib/containers/issueish-detail-container.js index b7022358e29..b253eae9aa7 100644 --- a/lib/containers/issueish-detail-container.js +++ b/lib/containers/issueish-detail-container.js @@ -169,6 +169,8 @@ export default class IssueishDetailContainer extends React.Component { addRemote={repository.addRemote.bind(repository)} onTitleChange={this.props.onTitleChange} switchToIssueish={this.props.switchToIssueish} + workdirPath={repository.getWorkingDirectoryPath()} + workspace={this.props.workspace} /> ); } diff --git a/lib/controllers/issueish-detail-controller.js b/lib/controllers/issueish-detail-controller.js index 2db4fba4981..4267f3b46f2 100644 --- a/lib/controllers/issueish-detail-controller.js +++ b/lib/controllers/issueish-detail-controller.js @@ -6,7 +6,8 @@ import {BranchSetPropType, RemoteSetPropType} from '../prop-types'; import {GitError} from '../git-shell-out-strategy'; import EnableableOperation from '../models/enableable-operation'; import IssueishDetailView, {checkoutStates} from '../views/issueish-detail-view'; -import {incrementCounter} from '../reporter-proxy'; +import CommitDetailItem from '../items/commit-detail-item'; +import {incrementCounter, addEvent} from '../reporter-proxy'; export class BareIssueishDetailController extends React.Component { static propTypes = { @@ -16,6 +17,7 @@ export class BareIssueishDetailController extends React.Component { login: PropTypes.string.isRequired, }).isRequired, issueish: PropTypes.any, // FIXME from IssueishPaneItemContainer.propTypes + getWorkingDirectoryPath: PropTypes.func.isRequired, }), issueishNumber: PropTypes.number.isRequired, @@ -33,6 +35,9 @@ export class BareIssueishDetailController extends React.Component { addRemote: PropTypes.func.isRequired, onTitleChange: PropTypes.func.isRequired, switchToIssueish: PropTypes.func.isRequired, + + workspace: PropTypes.object.isRequired, + workdirPath: PropTypes.string.isRequired, } constructor(props) { @@ -85,6 +90,7 @@ export class BareIssueishDetailController extends React.Component { issueish={repository.issueish} checkoutOp={this.checkoutOp} switchToIssueish={this.props.switchToIssueish} + openCommit={this.openCommit} /> ); } @@ -201,6 +207,13 @@ export class BareIssueishDetailController extends React.Component { incrementCounter('checkout-pr'); } + + openCommit = ({sha}) => { + const uri = CommitDetailItem.buildURI(this.props.workdirPath, sha); + this.props.workspace.open(uri, {pending: true}).then(() => { + addEvent('open-commit-in-pane', {package: 'github', from: this.constructor.name}); + }); + } } export default createFragmentContainer(BareIssueishDetailController, { diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 282150c71d9..0977f7c5ade 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -415,6 +415,8 @@ export default class RootController extends React.Component { workingDirectory={params.workingDirectory} workdirContextPool={this.props.workdirContextPool} loginModel={this.props.loginModel} + + workspace={this.props.workspace} /> )} diff --git a/lib/items/issueish-detail-item.js b/lib/items/issueish-detail-item.js index 88b8242b71e..88c0bd78d3a 100644 --- a/lib/items/issueish-detail-item.js +++ b/lib/items/issueish-detail-item.js @@ -18,6 +18,8 @@ export default class IssueishDetailItem extends Component { workingDirectory: PropTypes.string.isRequired, workdirContextPool: WorkdirContextPoolPropType.isRequired, loginModel: GithubLoginModelPropType.isRequired, + + workspace: PropTypes.object.isRequired, } static uriPattern = 'atom-github://issueish/{host}/{owner}/{repo}/{issueishNumber}?workdir={workingDirectory}' @@ -66,6 +68,7 @@ export default class IssueishDetailItem extends Component { issueishNumber={this.state.issueishNumber} repository={this.state.repository} + workspace={this.props.workspace} loginModel={this.props.loginModel} onTitleChange={this.handleTitleChanged} diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index 55d5a148e75..940cabd3be3 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -143,6 +143,10 @@ export class BareIssueishDetailView extends React.Component { } renderPullRequestBody(issueish, childProps) { + const {checkoutOp} = this.props; + const reason = checkoutOp.why(); + const onBranch = reason && !reason({hidden: true, default: false}); // is there a more direct way than this? + return ( @@ -184,7 +188,7 @@ export class BareIssueishDetailView extends React.Component { {/* commits */} - + ); diff --git a/lib/views/pr-commit-view.js b/lib/views/pr-commit-view.js index 88d879a3cbf..3e6d2a036f0 100644 --- a/lib/views/pr-commit-view.js +++ b/lib/views/pr-commit-view.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import {emojify} from 'node-emoji'; import moment from 'moment'; import {graphql, createFragmentContainer} from 'react-relay'; +import cx from 'classnames'; import {autobind} from '../helpers'; @@ -37,6 +38,12 @@ export class PrCommitView extends React.Component { return moment(date).fromNow(); } + openCommitDetailItem = () => { + if (this.props.onBranch) { + return this.props.openCommit({sha: this.props.item.abbreviatedOid}); + } + } + render() { const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; const {avatarUrl, name, date} = this.props.item.committer; @@ -44,7 +51,10 @@ export class PrCommitView extends React.Component {

    - {emojify(messageHeadline)} + + {emojify(messageHeadline)} + {messageBody ?

    ); } else if (this.props.repository.hasDirectory() && - !isValidWorkdir(this.props.repository.getWorkingDirectoryPath())) { + !isValidWorkdir(this.props.repository.getWorkingDirectoryPath())) { return (
    @@ -295,7 +293,7 @@ export default class GitTabView extends React.Component { } focusAndSelectRecentCommit() { - this.setFocus(RecentCommitsView.focus.RECENT_COMMIT); + this.setFocus(RecentCommitsController.focus.RECENT_COMMIT); } focusAndSelectCommitPreviewButton() { From 6fd2eb59b71ae8b9e7dd54c0babb4ef41e22c299 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 09:57:17 -0500 Subject: [PATCH 099/117] Touch up PropTypes in CommitDetail components --- lib/containers/commit-detail-container.js | 1 + lib/controllers/commit-detail-controller.js | 2 +- lib/views/commit-detail-view.js | 16 +++++++++------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/containers/commit-detail-container.js b/lib/containers/commit-detail-container.js index 275d82ce353..c624d127f79 100644 --- a/lib/containers/commit-detail-container.js +++ b/lib/containers/commit-detail-container.js @@ -10,6 +10,7 @@ export default class CommitDetailContainer extends React.Component { static propTypes = { repository: PropTypes.object.isRequired, sha: PropTypes.string.isRequired, + itemType: PropTypes.func.isRequired, } fetchData = repository => { diff --git a/lib/controllers/commit-detail-controller.js b/lib/controllers/commit-detail-controller.js index 6e0df0c146c..3f6014b9c79 100644 --- a/lib/controllers/commit-detail-controller.js +++ b/lib/controllers/commit-detail-controller.js @@ -5,7 +5,7 @@ import CommitDetailView from '../views/commit-detail-view'; export default class CommitDetailController extends React.Component { static propTypes = { - ...CommitDetailView.propTypes, + ...CommitDetailView.drilledPropTypes, commit: PropTypes.object.isRequired, } diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index 40a5280618c..cdd383e616b 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -4,13 +4,15 @@ import {emojify} from 'node-emoji'; import moment from 'moment'; import MultiFilePatchController from '../controllers/multi-file-patch-controller'; -import CommitDetailItem from '../items/commit-detail-item'; export default class CommitDetailView extends React.Component { - static propTypes = { + static drilledPropTypes = { repository: PropTypes.object.isRequired, commit: PropTypes.object.isRequired, - itemType: PropTypes.oneOf([CommitDetailItem]).isRequired, + currentRemote: PropTypes.object.isRequired, + currentBranch: PropTypes.object.isRequired, + isCommitPushed: PropTypes.bool.isRequired, + itemType: PropTypes.func.isRequired, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, @@ -19,14 +21,14 @@ export default class CommitDetailView extends React.Component { config: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, + } + + static propTypes = { + ...CommitDetailView.drilledPropTypes, messageCollapsible: PropTypes.bool.isRequired, messageOpen: PropTypes.bool.isRequired, toggleMessage: PropTypes.func.isRequired, - - currentRemote: PropTypes.object.isRequired, - currentBranch: PropTypes.object.isRequired, - isCommitPushed: PropTypes.bool.isRequired, } render() { From 5c1389c6c4e27fed55a0feba1f3dac90cbd0a7f2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:13:47 -0500 Subject: [PATCH 100/117] Forward focus management from RecentCommitsController to its view --- lib/controllers/recent-commits-controller.js | 15 ++++++++++++++- .../controllers/recent-commits-controller.test.js | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index cd846ca89eb..041cfb8ad7c 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import {addEvent} from '../reporter-proxy'; +import {CompositeDisposable} from 'event-kit'; import CommitDetailItem from '../items/commit-detail-item'; import URIPattern from '../atom/uri-pattern'; import RecentCommitsView from '../views/recent-commits-view'; -import {CompositeDisposable} from 'event-kit'; +import RefHolder from '../models/ref-holder'; export default class RecentCommitsController extends React.Component { static propTypes = { @@ -25,6 +26,9 @@ export default class RecentCommitsController extends React.Component { this.subscriptions = new CompositeDisposable( this.props.workspace.onDidChangeActivePaneItem(this.updateSelectedCommit), ); + + this.refView = new RefHolder(); + this.state = {selectedCommitSha: ''}; } @@ -50,6 +54,7 @@ export default class RecentCommitsController extends React.Component { render() { return ( view.rememberFocus(event)).getOr(null); + } + + setFocus(focus) { + return this.refView.map(view => view.setFocus(focus)).getOr(false); + } } diff --git a/test/controllers/recent-commits-controller.test.js b/test/controllers/recent-commits-controller.test.js index d67d1985cbe..c566b7865c9 100644 --- a/test/controllers/recent-commits-controller.test.js +++ b/test/controllers/recent-commits-controller.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import {shallow} from 'enzyme'; +import {shallow, mount} from 'enzyme'; import RecentCommitsController from '../../lib/controllers/recent-commits-controller'; import {commitBuilder} from '../builder/commit'; @@ -127,4 +127,17 @@ describe('RecentCommitsController', function() { }); }); }); + + it('forwards focus management methods to its view', function() { + const wrapper = mount(app); + + const setFocusSpy = sinon.spy(wrapper.find('RecentCommitsView').instance(), 'setFocus'); + const rememberFocusSpy = sinon.spy(wrapper.find('RecentCommitsView').instance(), 'rememberFocus'); + + wrapper.instance().setFocus(RecentCommitsController.focus.RECENT_COMMIT); + assert.isTrue(setFocusSpy.calledWith(RecentCommitsController.focus.RECENT_COMMIT)); + + wrapper.instance().rememberFocus({target: null}); + assert.isTrue(rememberFocusSpy.calledWith({target: null})); + }); }); From 333958b30a2c54d25fbf3286873563e8a4c0024c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:14:25 -0500 Subject: [PATCH 101/117] Optionally preserve keyboard focus when opening commit details --- lib/controllers/recent-commits-controller.js | 5 ++++- .../recent-commits-controller.test.js | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index 041cfb8ad7c..ac4b6d97435 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -67,10 +67,13 @@ export default class RecentCommitsController extends React.Component { ); } - openCommit = async ({sha}) => { + openCommit = async ({sha, preserveFocus}) => { const workdir = this.props.repository.getWorkingDirectoryPath(); const uri = CommitDetailItem.buildURI(workdir, sha); await this.props.workspace.open(uri, {pending: true}); + if (preserveFocus) { + this.setFocus(this.constructor.focus.RECENT_COMMIT); + } addEvent('open-commit-in-pane', {package: 'github', from: this.constructor.name}); } diff --git a/test/controllers/recent-commits-controller.test.js b/test/controllers/recent-commits-controller.test.js index c566b7865c9..3875daf6d2b 100644 --- a/test/controllers/recent-commits-controller.test.js +++ b/test/controllers/recent-commits-controller.test.js @@ -44,7 +44,7 @@ describe('RecentCommitsController', function() { assert.isTrue(wrapper.find('RecentCommitsView').prop('isLoading')); }); - describe('openCommit({sha})', function() { + describe('openCommit({sha, preserveFocus})', function() { it('opens a commit detail item', async function() { sinon.stub(atomEnv.workspace, 'open').resolves(); @@ -53,7 +53,7 @@ describe('RecentCommitsController', function() { app = React.cloneElement(app, {commits}); const wrapper = shallow(app); - await wrapper.find('RecentCommitsView').prop('openCommit')({sha: 'asdf1234'}); + await wrapper.find('RecentCommitsView').prop('openCommit')({sha: 'asdf1234', preserveFocus: false}); assert.isTrue(atomEnv.workspace.open.calledWith( `atom-github://commit-detail?workdir=${encodeURIComponent(workdirPath)}` + @@ -61,6 +61,20 @@ describe('RecentCommitsController', function() { )); }); + it('preserves keyboard focus within the RecentCommitsView when requested', async function() { + sinon.stub(atomEnv.workspace, 'open').resolves(); + + const sha = 'asdf1234'; + const commits = [commitBuilder().sha(sha).build()]; + app = React.cloneElement(app, {commits}); + + const wrapper = mount(app); + const focusSpy = sinon.stub(wrapper.find('RecentCommitsView').instance(), 'setFocus').returns(true); + + await wrapper.find('RecentCommitsView').prop('openCommit')({sha: 'asdf1234', preserveFocus: true}); + assert.isTrue(focusSpy.called); + }); + it('records an event', async function() { sinon.stub(atomEnv.workspace, 'open').resolves(); sinon.stub(reporterProxy, 'addEvent'); From d1be87da8a204b34f026b518512ed9ce2dccaf43 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:26:41 -0500 Subject: [PATCH 102/117] Use the CommitBuilder in RecentCommitView tests --- test/views/recent-commits-view.test.js | 84 +++++++++++++------------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index 99ae103ddfb..19a1ca99073 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -2,13 +2,30 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; import RecentCommitsView from '../../lib/views/recent-commits-view'; -import Commit from '../../lib/models/commit'; +import {commitBuilder} from '../builder/commit'; describe('RecentCommitsView', function() { - let app; + let atomEnv, app; beforeEach(function() { - app = {}} isLoading={false} />; + atomEnv = global.buildAtomEnvironment(); + + app = ( + { }} + openCommit={() => { }} + selectNextCommit={() => { }} + selectPreviousCommit={() => { }} + /> + ); + }); + + afterEach(function() { + atomEnv.destroy(); }); it('shows a placeholder while commits are empty and loading', function() { @@ -28,11 +45,7 @@ describe('RecentCommitsView', function() { }); it('renders a RecentCommitView for each commit', function() { - const commits = ['1', '2', '3'].map(sha => { - return { - getSha() { return sha; }, - }; - }); + const commits = ['1', '2', '3'].map(sha => commitBuilder().sha(sha).build()); app = React.cloneElement(app, {commits}); const wrapper = shallow(app); @@ -41,20 +54,19 @@ describe('RecentCommitsView', function() { }); it('renders emojis in the commit subject', function() { - const commits = [new Commit({ - sha: '1111111111', - authorEmail: 'pizza@unicorn.com', - authorDate: 0, - messageSubject: ':heart: :shirt: :smile:', - })]; + const commits = [commitBuilder().messageSubject(':heart: :shirt: :smile:').build()]; + app = React.cloneElement(app, {commits}); const wrapper = mount(app); - assert.deepEqual(wrapper.find('.github-RecentCommit-message').text(), '❤️ 👕 😄'); + assert.strictEqual(wrapper.find('.github-RecentCommit-message').text(), '❤️ 👕 😄'); }); it('renders an avatar corresponding to the GitHub user who authored the commit', function() { const commits = ['thr&ee@z.com', 'two@y.com', 'one@x.com'].map((authorEmail, i) => { - return new Commit({sha: '1111111111' + i, authorEmail, authorDate: 0, message: 'x'}); + return commitBuilder() + .sha(`1111111111${i}`) + .authorEmail(authorEmail) + .build(); }); app = React.cloneElement(app, {commits}); @@ -70,13 +82,13 @@ 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: [{name: 'One', email: 'two@y.com'}, {name: 'Two', email: 'one@x.com'}], - })]; + const commits = [ + commitBuilder() + .authorEmail('thr&ee@z.com') + .addCoAuthor('One', 'two@y.com') + .addCoAuthor('Two', 'one@x.com') + .build(), + ]; app = React.cloneElement(app, {commits}); const wrapper = mount(app); @@ -91,12 +103,7 @@ 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', - }); + const commit = commitBuilder().authorDate(1519848555).build(); app = React.cloneElement(app, {commits: [commit]}); const wrapper = mount(app); @@ -104,13 +111,7 @@ describe('RecentCommitsView', function() { }); it('renders emoji in the title attribute', function() { - const commit = new Commit({ - sha: '1111111111', - authorEmail: 'me@hooray.horse', - authorDate: 0, - messageSubject: ':heart:', - messageBody: 'and a commit body', - }); + const commit = commitBuilder().messageSubject(':heart:').messageBody('and a commit body').build(); app = React.cloneElement(app, {commits: [commit]}); const wrapper = mount(app); @@ -122,13 +123,10 @@ describe('RecentCommitsView', function() { }); it('renders the full commit message in a title attribute', function() { - const commit = new Commit({ - sha: '1111111111', - authorEmail: 'me@hooray.horse', - authorDate: 0, - messageSubject: 'really really really really really really really long', - messageBody: 'and a commit body', - }); + const commit = commitBuilder() + .messageSubject('really really really really really really really long') + .messageBody('and a commit body') + .build(); app = React.cloneElement(app, {commits: [commit]}); const wrapper = mount(app); From 1cea7a5c1621226ae18643c3f62c1075855b4322 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:44:02 -0500 Subject: [PATCH 103/117] RecentCommitView keyboard navigation --- lib/views/recent-commits-view.js | 41 +++++++++++++++--------- test/views/recent-commits-view.test.js | 43 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 9faffd716df..f07f9b21e7d 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -92,12 +92,19 @@ class RecentCommitView extends React.Component { export default class RecentCommitsView extends React.Component { static propTypes = { + // Model state commits: PropTypes.arrayOf(PropTypes.object).isRequired, isLoading: PropTypes.bool.isRequired, - undoLastCommit: PropTypes.func.isRequired, - openCommit: PropTypes.func.isRequired, selectedCommitSha: PropTypes.string.isRequired, + + // Atom environment commandRegistry: PropTypes.object.isRequired, + + // Action methods + undoLastCommit: PropTypes.func.isRequired, + openCommit: PropTypes.func.isRequired, + selectNextCommit: PropTypes.func.isRequired, + selectPreviousCommit: PropTypes.func.isRequired, }; static focus = { @@ -106,27 +113,30 @@ export default class RecentCommitsView extends React.Component { constructor(props) { super(props); - this.refRecentCommits = new RefHolder(); + this.refRoot = new RefHolder(); } setFocus(focus) { - return this.refRecentCommits.map(view => view.setFocus(focus)).getOr(false); - } + if (focus === this.constructor.focus.RECENT_COMMIT) { + return this.refRoot.map(element => element.focus()).getOr(false); + } - rememberFocus(event) { - return this.refRecentCommits.map(view => view.rememberFocus(event)).getOr(null); + return false; } - selectNextCommit() { - // okay, we should probably move the state of the selected commit into this component - // instead of using the sha, so we can more easily move to next / previous. + rememberFocus(event) { + return this.refRoot.map(element => element.contains(event.target)).getOr(false) + ? this.constructor.focus.RECENT_COMMIT + : null; } render() { return ( -
    - - +
    + + + + {this.renderCommits()}
    @@ -158,7 +168,7 @@ export default class RecentCommitsView extends React.Component { isMostRecent={i === 0} commit={commit} undoLastCommit={this.props.undoLastCommit} - openCommit={() => this.props.openCommit({sha: commit.getSha()})} + openCommit={() => this.props.openCommit({sha: commit.getSha(), preserveFocus: true})} isSelected={this.props.selectedCommitSha === commit.getSha()} /> ); @@ -166,6 +176,7 @@ export default class RecentCommitsView extends React.Component { ); } - } + + openSelectedCommit = () => this.props.openCommit({sha: this.props.selectedCommitSha, preserveFocus: false}) } diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index 19a1ca99073..81e01ad5623 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -137,4 +137,47 @@ describe('RecentCommitsView', function() { 'and a commit body', ); }); + + it('opens a commit on click, preserving keyboard focus', function() { + const openCommit = sinon.spy(); + const commits = [ + commitBuilder().sha('0').build(), + commitBuilder().sha('1').build(), + commitBuilder().sha('2').build(), + ]; + const wrapper = mount(React.cloneElement(app, {commits, openCommit, selectedCommitSha: '2'})); + + wrapper.find('RecentCommitView').at(1).simulate('click'); + + assert.isTrue(openCommit.calledWith({sha: '1', preserveFocus: true})); + }); + + describe('keybindings', function() { + it('advances to the next commit on core:move-down', function() { + const selectNextCommit = sinon.spy(); + const wrapper = mount(React.cloneElement(app, {selectNextCommit})); + + atomEnv.commands.dispatch(wrapper.getDOMNode(), 'core:move-down'); + + assert.isTrue(selectNextCommit.called); + }); + + it('retreats to the previous commit on core:move-up', function() { + const selectPreviousCommit = sinon.spy(); + const wrapper = mount(React.cloneElement(app, {selectPreviousCommit})); + + atomEnv.commands.dispatch(wrapper.getDOMNode(), 'core:move-up'); + + assert.isTrue(selectPreviousCommit.called); + }); + + it('opens the currently selected commit and does not preserve focus on core:confirm', function() { + const openCommit = sinon.spy(); + const wrapper = mount(React.cloneElement(app, {openCommit, selectedCommitSha: '1234'})); + + atomEnv.commands.dispatch(wrapper.getDOMNode(), 'core:confirm'); + + assert.isTrue(openCommit.calledWith({sha: '1234', preserveFocus: false})); + }); + }); }); From 3cafdc1968bed5448ef12e20740a9993b68bf954 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:44:13 -0500 Subject: [PATCH 104/117] Forgot to commit part of this test, whoops --- test/controllers/recent-commits-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/recent-commits-controller.test.js b/test/controllers/recent-commits-controller.test.js index 3875daf6d2b..89b4c13578b 100644 --- a/test/controllers/recent-commits-controller.test.js +++ b/test/controllers/recent-commits-controller.test.js @@ -84,7 +84,7 @@ describe('RecentCommitsController', function() { app = React.cloneElement(app, {commits}); const wrapper = shallow(app); - await wrapper.instance().openCommit({sha: 'asdf1234'}); + await wrapper.instance().openCommit({sha: 'asdf1234', preserveFocus: true}); assert.isTrue(reporterProxy.addEvent.calledWith('open-commit-in-pane', { package: 'github', from: RecentCommitsController.name, From c90e2a56d10375d54f86fee1413078ddc7c4d6eb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:54:31 -0500 Subject: [PATCH 105/117] Scroll commits into view as you select them --- lib/views/recent-commits-view.js | 19 +++++++++++++++++++ test/views/recent-commits-view.test.js | 12 ++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index f07f9b21e7d..99d84a7f0bc 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -18,12 +18,31 @@ class RecentCommitView extends React.Component { isSelected: PropTypes.bool.isRequired, }; + constructor(props) { + super(props); + + this.refRoot = new RefHolder(); + } + + componentDidMount() { + if (this.props.isSelected) { + this.refRoot.map(root => root.scrollIntoViewIfNeeded(false)); + } + } + + componentDidUpdate(prevProps) { + if (this.props.isSelected && !prevProps.isSelected) { + this.refRoot.map(root => root.scrollIntoViewIfNeeded(false)); + } + } + render() { const authorMoment = moment(this.props.commit.getAuthorDate() * 1000); const fullMessage = this.props.commit.getFullMessage(); return (
  • w.prop('commit')), commits); }); + it('scrolls the selected RecentCommitView into visibility', function() { + const commits = ['0', '1', '2', '3'].map(sha => commitBuilder().sha(sha).build()); + + app = React.cloneElement(app, {commits, selectedCommitSha: '1'}); + const wrapper = mount(app); + const scrollSpy = sinon.spy(wrapper.find('RecentCommitView').at(3).getDOMNode(), 'scrollIntoViewIfNeeded'); + + wrapper.setProps({selectedCommitSha: '3'}); + + assert.isTrue(scrollSpy.calledWith(false)); + }); + it('renders emojis in the commit subject', function() { const commits = [commitBuilder().messageSubject(':heart: :shirt: :smile:').build()]; From a37a3c861bd27cb4bb238c587f64090952577f6f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 10:56:37 -0500 Subject: [PATCH 106/117] Keymap entries for diving and surfacing from commits --- keymaps/git.cson | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/keymaps/git.cson b/keymaps/git.cson index f6236b9a07c..16f31c82d63 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -32,11 +32,6 @@ 'tab': 'core:focus-next' 'shift-tab': 'core:focus-previous' -'.github-RecentCommitsView': - 'up': 'github:recent-commit-up' - 'down': 'github:recent-commit-down' - 'enter': 'github:open-recent-commit' - '.github-StagingView.unstaged-changes-focused': 'cmd-backspace': 'github:discard-changes-in-selected-files' 'ctrl-backspace': 'github:discard-changes-in-selected-files' @@ -51,6 +46,15 @@ 'ctrl-left': 'github:dive' 'enter': 'native!' +'.github-RecentCommits': + 'enter': 'github:dive' + 'cmd-left': 'github:dive' + 'ctrl-left': 'github:dive' + +'.github-CommitDetailView': + 'cmd-right': 'github:surface' + 'ctrl-right': 'github:surface' + '.github-FilePatchView atom-text-editor:not([mini])': 'cmd-/': 'github:toggle-patch-selection-mode' 'ctrl-/': 'github:toggle-patch-selection-mode' From 643c7fda79061ffd33abe80eddacb83043dd849d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 11:00:40 -0500 Subject: [PATCH 107/117] Use github:dive instead of core:confirm for consistency --- lib/views/recent-commits-view.js | 2 +- test/views/recent-commits-view.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 99d84a7f0bc..dfd60105fc2 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -155,7 +155,7 @@ export default class RecentCommitsView extends React.Component { - + {this.renderCommits()}
  • diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index 540ef46b3c6..31c66257034 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -183,11 +183,11 @@ describe('RecentCommitsView', function() { assert.isTrue(selectPreviousCommit.called); }); - it('opens the currently selected commit and does not preserve focus on core:confirm', function() { + it('opens the currently selected commit and does not preserve focus on github:dive', function() { const openCommit = sinon.spy(); const wrapper = mount(React.cloneElement(app, {openCommit, selectedCommitSha: '1234'})); - atomEnv.commands.dispatch(wrapper.getDOMNode(), 'core:confirm'); + atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:dive'); assert.isTrue(openCommit.calledWith({sha: '1234', preserveFocus: false})); }); From 4852d709ce42708eea9033fa94f53feee15ddc25 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 11:46:30 -0500 Subject: [PATCH 108/117] Focus and select the current recent commit --- lib/controllers/git-tab-controller.js | 4 +++ lib/controllers/root-controller.js | 8 +++++- lib/items/git-tab-item.js | 4 +++ lib/views/commit-detail-view.js | 27 ++++++++++++++++++- test/controllers/git-tab-controller.test.js | 9 +++++++ test/controllers/root-controller.test.js | 22 ++++++++++++++++ test/items/git-tab-item.test.js | 1 + test/views/commit-detail-view.test.js | 29 ++++++++++++++++++--- 8 files changed, 98 insertions(+), 6 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index f3322f4c797..c1d732df4fc 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -363,6 +363,10 @@ export default class GitTabController extends React.Component { return this.refView.map(view => view.focusAndSelectCommitPreviewButton()); } + focusAndSelectRecentCommit() { + return this.refView.map(view => view.focusAndSelectRecentCommit()); + } + quietlySelectItem(filePath, stagingStatus) { return this.refView.map(view => view.quietlySelectItem(filePath, stagingStatus)).getOr(null); } diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 2b5bc84e0cc..350c25d76ff 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -100,7 +100,7 @@ export default class RootController extends React.Component { this.props.commandRegistry.onDidDispatch(event => { if (event.type && event.type.startsWith('github:') - && event.detail && event.detail[0] && event.detail[0].contextCommand) { + && event.detail && event.detail[0] && event.detail[0].contextCommand) { addEvent('context-menu-action', { package: 'github', command: event.type, @@ -399,6 +399,7 @@ export default class RootController extends React.Component { config={this.props.config} sha={params.sha} + surfaceCommit={this.surfaceToRecentCommit} /> )} @@ -617,6 +618,11 @@ export default class RootController extends React.Component { return gitTab && gitTab.focusAndSelectCommitPreviewButton(); } + surfaceToRecentCommit = () => { + const gitTab = this.gitTabTracker.getComponent(); + return gitTab && gitTab.focusAndSelectRecentCommit(); + } + destroyFilePatchPaneItems() { destroyFilePatchPaneItems({onlyStaged: false}, this.props.workspace); } diff --git a/lib/items/git-tab-item.js b/lib/items/git-tab-item.js index 254c9d717aa..e8de6d7ccad 100644 --- a/lib/items/git-tab-item.js +++ b/lib/items/git-tab-item.js @@ -90,4 +90,8 @@ export default class GitTabItem extends React.Component { quietlySelectItem(...args) { return this.refController.map(c => c.quietlySelectItem(...args)); } + + focusAndSelectRecentCommit() { + return this.refController.map(c => c.focusAndSelectRecentCommit()); + } } diff --git a/lib/views/commit-detail-view.js b/lib/views/commit-detail-view.js index cdd383e616b..af819f94428 100644 --- a/lib/views/commit-detail-view.js +++ b/lib/views/commit-detail-view.js @@ -4,9 +4,12 @@ import {emojify} from 'node-emoji'; import moment from 'moment'; import MultiFilePatchController from '../controllers/multi-file-patch-controller'; +import Commands, {Command} from '../atom/commands'; +import RefHolder from '../models/ref-holder'; export default class CommitDetailView extends React.Component { static drilledPropTypes = { + // Model properties repository: PropTypes.object.isRequired, commit: PropTypes.object.isRequired, currentRemote: PropTypes.object.isRequired, @@ -14,28 +17,41 @@ export default class CommitDetailView extends React.Component { isCommitPushed: PropTypes.bool.isRequired, itemType: PropTypes.func.isRequired, + // Atom environment workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, keymaps: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, config: PropTypes.object.isRequired, + // Action functions destroy: PropTypes.func.isRequired, + surfaceCommit: PropTypes.func.isRequired, } static propTypes = { ...CommitDetailView.drilledPropTypes, + // Controller state messageCollapsible: PropTypes.bool.isRequired, messageOpen: PropTypes.bool.isRequired, + + // Action functions toggleMessage: PropTypes.func.isRequired, } + constructor(props) { + super(props); + + this.refRoot = new RefHolder(); + } + render() { const commit = this.props.commit; return ( -
    +
    + {this.renderCommands()}

    @@ -58,12 +74,21 @@ export default class CommitDetailView extends React.Component {

    ); } + renderCommands() { + return ( + + + + ); + } + renderCommitMessageBody() { const collapsed = this.props.messageCollapsible && !this.props.messageOpen; diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index facd99b14b2..68791712fba 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -221,6 +221,15 @@ describe('GitTabController', function() { assert.isTrue(focusMethod.called); }); + it('imperatively selects the recent commit', async function() { + const repository = await buildRepository(await cloneRepository('three-files')); + const wrapper = mount(await buildApp(repository)); + + const focusMethod = sinon.spy(wrapper.find('GitTabView').instance(), 'focusAndSelectRecentCommit'); + wrapper.instance().focusAndSelectRecentCommit(); + assert.isTrue(focusMethod.called); + }); + describe('focus management', function() { it('remembers the last focus reported by the view', async function() { const repository = await buildRepository(await cloneRepository()); diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index e5203adadcd..67963177815 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -1314,4 +1314,26 @@ describe('RootController', function() { }); }); + describe('surfaceToRecentCommit', function() { + it('focuses and selects the recent commit', async function() { + const repository = await buildRepository(await cloneRepository('multiple-commits')); + app = React.cloneElement(app, { + repository, + startOpen: true, + startRevealed: true, + }); + const wrapper = mount(app); + + const gitTabTracker = wrapper.instance().gitTabTracker; + + const gitTab = { + focusAndSelectRecentCommit: sinon.spy(), + }; + sinon.stub(gitTabTracker, 'getComponent').returns(gitTab); + + wrapper.instance().surfaceToRecentCommit(); + assert.isTrue(gitTab.focusAndSelectRecentCommit.called); + }); + }); + }); diff --git a/test/items/git-tab-item.test.js b/test/items/git-tab-item.test.js index 09be4becd05..4bd740b459f 100644 --- a/test/items/git-tab-item.test.js +++ b/test/items/git-tab-item.test.js @@ -61,6 +61,7 @@ describe('GitTabItem', function() { const focusMethods = [ 'focusAndSelectStagingItem', 'focusAndSelectCommitPreviewButton', + 'focusAndSelectRecentCommit', ]; const spies = focusMethods.reduce((map, focusMethod) => { diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js index e56c35a0427..ce640036985 100644 --- a/test/views/commit-detail-view.test.js +++ b/test/views/commit-detail-view.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import {shallow} from 'enzyme'; +import {shallow, mount} from 'enzyme'; import moment from 'moment'; import dedent from 'dedent-js'; @@ -24,7 +24,7 @@ describe('CommitDetailView', function() { function buildApp(override = {}) { const props = { repository, - commit: commitBuilder().build(), + commit: commitBuilder().setMultiFileDiff().build(), messageCollapsible: false, messageOpen: true, itemType: CommitDetailItem, @@ -35,8 +35,9 @@ describe('CommitDetailView', function() { tooltips: atomEnv.tooltips, config: atomEnv.config, - destroy: () => {}, - toggleMessage: () => {}, + destroy: () => { }, + toggleMessage: () => { }, + surfaceCommit: () => { }, ...override, }; @@ -206,4 +207,24 @@ describe('CommitDetailView', function() { }); }); }); + + describe('keyboard bindings', function() { + it('surfaces the recent commit on github:surface', function() { + const surfaceCommit = sinon.spy(); + const wrapper = mount(buildApp({surfaceCommit})); + + atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:surface'); + + assert.isTrue(surfaceCommit.called); + }); + + it('surfaces from the embedded MultiFilePatchView', function() { + const surfaceCommit = sinon.spy(); + const wrapper = mount(buildApp({surfaceCommit})); + + atomEnv.commands.dispatch(wrapper.find('.github-FilePatchView').getDOMNode(), 'github:surface'); + + assert.isTrue(surfaceCommit.called); + }); + }); }); From 2a2b9b0f83dcc98bb9fe608855733e23c6a66cde Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 11:46:45 -0500 Subject: [PATCH 109/117] Set and remember focus on the RecentCommitView --- lib/views/git-tab-view.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index a2da551840e..1330e299c0e 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -70,6 +70,7 @@ export default class GitTabView extends React.Component { this.subscriptions = new CompositeDisposable(); this.refCommitController = new RefHolder(); + this.refRecentCommitsController = new RefHolder(); } componentDidMount() { @@ -193,6 +194,7 @@ export default class GitTabView extends React.Component { updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} /> controller.rememberFocus(event)).getOr(null); } + if (!currentFocus) { + currentFocus = this.refRecentCommitsController.map(controller => controller.rememberFocus(event)).getOr(null); + } + return currentFocus; } @@ -243,6 +249,10 @@ export default class GitTabView extends React.Component { return true; } + if (this.refRecentCommitsController.map(controller => controller.setFocus(focus)).getOr(false)) { + return true; + } + return false; } From 74ef8a8eb6a6f617364d77333a915a97f9500b9f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 11:48:48 -0500 Subject: [PATCH 110/117] Focus refInitialFocus even if it isn't available right away --- lib/items/commit-detail-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/items/commit-detail-item.js b/lib/items/commit-detail-item.js index fdfc39e6382..a7331b3cde8 100644 --- a/lib/items/commit-detail-item.js +++ b/lib/items/commit-detail-item.js @@ -89,6 +89,6 @@ export default class CommitDetailItem extends React.Component { } focus() { - this.refInitialFocus.map(focusable => focusable.focus()); + this.refInitialFocus.getPromise().then(focusable => focusable.focus()); } } From 3e65b6e9ed0b6e0f54e24d70fb6ba8de4ef537a8 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 30 Nov 2018 18:43:27 +0100 Subject: [PATCH 111/117] slight styling for title of commit in PR to look clickable --- styles/pr-commit-view.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/styles/pr-commit-view.less b/styles/pr-commit-view.less index 8a6d4e49bbb..0863199eecf 100644 --- a/styles/pr-commit-view.less +++ b/styles/pr-commit-view.less @@ -42,6 +42,10 @@ margin: 0 0 .25em 0; font-size: 1.2em; line-height: 1.4; + &:hover { + cursor: pointer; + text-decoration: underline; + } } &-avatar { From 51bd40d4fd6b42efde81462c645183570b1dc7d8 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 30 Nov 2018 20:09:38 +0100 Subject: [PATCH 112/117] add oid field to pr commit query & rename abbreviatedOid to shortSha for clarity --- .../issueishDetailContainerQuery.graphql.js | 16 ++++++++++++---- .../issueishDetailViewRefetchQuery.graphql.js | 16 ++++++++++++---- .../__generated__/prCommitView_item.graphql.js | 14 +++++++++++--- .../__generated__/prCommitsViewQuery.graphql.js | 16 ++++++++++++---- lib/views/pr-commit-view.js | 14 ++++++++------ 5 files changed, 55 insertions(+), 21 deletions(-) diff --git a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js index 73d262cf8c5..8e537f3d564 100644 --- a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash fe501fb51956752e2d45ff061c36622e + * @relayHash d760438e6f0650bc4ba45ae430d56116 */ /* eslint-disable */ @@ -476,7 +476,8 @@ fragment prCommitView_item on Commit { } messageHeadline messageBody - abbreviatedOid + shortSha: abbreviatedOid + sha: oid url } */ @@ -997,7 +998,7 @@ return { "operationKind": "query", "name": "issueishDetailContainerQuery", "id": null, - "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n $commitCount: Int!\n $commitCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_1mXVvq\n id\n }\n}\n\nfragment issueishDetailController_repository_1mXVvq on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_4cAEh0\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_4cAEh0\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_4cAEh0 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n isCrossRepository\n changedFiles\n ...prCommitsView_pullRequest_38TpXw\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prCommitsView_pullRequest_38TpXw on PullRequest {\n url\n commits(first: $commitCount, after: $commitCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n commit {\n id\n ...prCommitView_item\n }\n id\n __typename\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n recentCommits: commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n\nfragment prCommitView_item on Commit {\n committer {\n avatarUrl\n name\n date\n }\n messageHeadline\n messageBody\n abbreviatedOid\n url\n}\n", + "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n $commitCount: Int!\n $commitCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_1mXVvq\n id\n }\n}\n\nfragment issueishDetailController_repository_1mXVvq on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_4cAEh0\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_4cAEh0\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_4cAEh0 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n isCrossRepository\n changedFiles\n ...prCommitsView_pullRequest_38TpXw\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prCommitsView_pullRequest_38TpXw on PullRequest {\n url\n commits(first: $commitCount, after: $commitCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n commit {\n id\n ...prCommitView_item\n }\n id\n __typename\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n recentCommits: commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n\nfragment prCommitView_item on Commit {\n committer {\n avatarUrl\n name\n date\n }\n messageHeadline\n messageBody\n shortSha: abbreviatedOid\n sha: oid\n url\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -1169,11 +1170,18 @@ return { }, { "kind": "ScalarField", - "alias": null, + "alias": "shortSha", "name": "abbreviatedOid", "args": null, "storageKey": null }, + { + "kind": "ScalarField", + "alias": "sha", + "name": "oid", + "args": null, + "storageKey": null + }, v12 ] }, diff --git a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js index 3b5eeaf1e03..d300687d263 100644 --- a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 2918a11cbaa8f9b4f358a3913dab315c + * @relayHash 7a789201c89c0ffa32e9490d1bf7c181 */ /* eslint-disable */ @@ -446,7 +446,8 @@ fragment prCommitView_item on Commit { } messageHeadline messageBody - abbreviatedOid + shortSha: abbreviatedOid + sha: oid url } */ @@ -947,7 +948,7 @@ return { "operationKind": "query", "name": "issueishDetailViewRefetchQuery", "id": null, - "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n $commitCount: Int!\n $commitCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_4cAEh0\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_4cAEh0 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n isCrossRepository\n changedFiles\n ...prCommitsView_pullRequest_38TpXw\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prCommitsView_pullRequest_38TpXw on PullRequest {\n url\n commits(first: $commitCount, after: $commitCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n commit {\n id\n ...prCommitView_item\n }\n id\n __typename\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n recentCommits: commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n\nfragment prCommitView_item on Commit {\n committer {\n avatarUrl\n name\n date\n }\n messageHeadline\n messageBody\n abbreviatedOid\n url\n}\n", + "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n $commitCount: Int!\n $commitCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_4cAEh0\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_4cAEh0 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n isCrossRepository\n changedFiles\n ...prCommitsView_pullRequest_38TpXw\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prCommitsView_pullRequest_38TpXw on PullRequest {\n url\n commits(first: $commitCount, after: $commitCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n commit {\n id\n ...prCommitView_item\n }\n id\n __typename\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n recentCommits: commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n\nfragment prCommitView_item on Commit {\n committer {\n avatarUrl\n name\n date\n }\n messageHeadline\n messageBody\n shortSha: abbreviatedOid\n sha: oid\n url\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -1155,11 +1156,18 @@ return { }, { "kind": "ScalarField", - "alias": null, + "alias": "shortSha", "name": "abbreviatedOid", "args": null, "storageKey": null }, + { + "kind": "ScalarField", + "alias": "sha", + "name": "oid", + "args": null, + "storageKey": null + }, v11 ] }, diff --git a/lib/views/__generated__/prCommitView_item.graphql.js b/lib/views/__generated__/prCommitView_item.graphql.js index c5d7709a260..2d1203207cb 100644 --- a/lib/views/__generated__/prCommitView_item.graphql.js +++ b/lib/views/__generated__/prCommitView_item.graphql.js @@ -18,7 +18,8 @@ export type prCommitView_item = {| |}, +messageHeadline: string, +messageBody: string, - +abbreviatedOid: string, + +shortSha: string, + +sha: any, +url: any, +$refType: prCommitView_item$ref, |}; @@ -80,11 +81,18 @@ const node/*: ConcreteFragment*/ = { }, { "kind": "ScalarField", - "alias": null, + "alias": "shortSha", "name": "abbreviatedOid", "args": null, "storageKey": null }, + { + "kind": "ScalarField", + "alias": "sha", + "name": "oid", + "args": null, + "storageKey": null + }, { "kind": "ScalarField", "alias": null, @@ -95,5 +103,5 @@ const node/*: ConcreteFragment*/ = { ] }; // prettier-ignore -(node/*: any*/).hash = 'c7c00b19a2fd2a18e4c1bab7f5f252ff'; +(node/*: any*/).hash = '2bd193bec5d758f465d9428ff3cd8a09'; module.exports = node; diff --git a/lib/views/__generated__/prCommitsViewQuery.graphql.js b/lib/views/__generated__/prCommitsViewQuery.graphql.js index f3c5e4ff134..7d20b34ac57 100644 --- a/lib/views/__generated__/prCommitsViewQuery.graphql.js +++ b/lib/views/__generated__/prCommitsViewQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 9ee1d26899a0ab5d2eeeee7dbd86fb3d + * @relayHash 86632ba0fe5f43bd343d798b397ad81b */ /* eslint-disable */ @@ -73,7 +73,8 @@ fragment prCommitView_item on Commit { } messageHeadline messageBody - abbreviatedOid + shortSha: abbreviatedOid + sha: oid url } */ @@ -147,7 +148,7 @@ return { "operationKind": "query", "name": "prCommitsViewQuery", "id": null, - "text": "query prCommitsViewQuery(\n $commitCount: Int!\n $commitCursor: String\n $url: URI!\n) {\n resource(url: $url) {\n __typename\n ... on PullRequest {\n ...prCommitsView_pullRequest_38TpXw\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment prCommitsView_pullRequest_38TpXw on PullRequest {\n url\n commits(first: $commitCount, after: $commitCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n commit {\n id\n ...prCommitView_item\n }\n id\n __typename\n }\n }\n }\n}\n\nfragment prCommitView_item on Commit {\n committer {\n avatarUrl\n name\n date\n }\n messageHeadline\n messageBody\n abbreviatedOid\n url\n}\n", + "text": "query prCommitsViewQuery(\n $commitCount: Int!\n $commitCursor: String\n $url: URI!\n) {\n resource(url: $url) {\n __typename\n ... on PullRequest {\n ...prCommitsView_pullRequest_38TpXw\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment prCommitsView_pullRequest_38TpXw on PullRequest {\n url\n commits(first: $commitCount, after: $commitCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n commit {\n id\n ...prCommitView_item\n }\n id\n __typename\n }\n }\n }\n}\n\nfragment prCommitView_item on Commit {\n committer {\n avatarUrl\n name\n date\n }\n messageHeadline\n messageBody\n shortSha: abbreviatedOid\n sha: oid\n url\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -331,11 +332,18 @@ return { }, { "kind": "ScalarField", - "alias": null, + "alias": "shortSha", "name": "abbreviatedOid", "args": null, "storageKey": null }, + { + "kind": "ScalarField", + "alias": "sha", + "name": "oid", + "args": null, + "storageKey": null + }, v4 ] }, diff --git a/lib/views/pr-commit-view.js b/lib/views/pr-commit-view.js index 3f2b3157bf5..ed7d865fc6e 100644 --- a/lib/views/pr-commit-view.js +++ b/lib/views/pr-commit-view.js @@ -19,7 +19,8 @@ export class PrCommitView extends React.Component { }).isRequired, messageBody: PropTypes.string, messageHeadline: PropTypes.string.isRequired, - abbreviatedOid: PropTypes.string.isRequired, + shortSha: PropTypes.string.isRequired, + sha: PropTypes.string.isRequired, url: PropTypes.string.isRequired, }).isRequired, onBranch: PropTypes.bool.isRequired, @@ -41,11 +42,11 @@ export class PrCommitView extends React.Component { } openCommitDetailItem = () => { - return this.props.onBranch ? this.props.openCommit({sha: this.props.item.abbreviatedOid}) : null; + return this.props.onBranch ? this.props.openCommit({sha: this.props.item.shortSha}) : null; } render() { - const {messageHeadline, messageBody, abbreviatedOid, url} = this.props.item; + const {messageHeadline, messageBody, shortSha, url} = this.props.item; const {avatarUrl, name, date} = this.props.item.committer; return (
    @@ -77,8 +78,8 @@ export class PrCommitView extends React.Component {
    @@ -96,7 +97,8 @@ export default createFragmentContainer(PrCommitView, { } messageHeadline messageBody - abbreviatedOid + shortSha: abbreviatedOid + sha: oid url }`, }); From 0bed1f6c77b2fb1a4b6b4093e2d8140457aa4b26 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 30 Nov 2018 20:12:04 +0100 Subject: [PATCH 113/117] fix test --- test/views/pr-commits-view.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/views/pr-commits-view.test.js b/test/views/pr-commits-view.test.js index 0d6e226d6c6..5b0cbf705ea 100644 --- a/test/views/pr-commits-view.test.js +++ b/test/views/pr-commits-view.test.js @@ -11,7 +11,8 @@ const commitSpec = { date: '2018-05-16T21:54:24.500Z', }, messageHeadline: 'This one weird trick for getting to the moon will blow your mind 🚀', - abbreviatedOid: 'bad1dea', + shortSha: 'bad1dea', + sha: 'bad1deaea3d816383721478fc631b5edd0c2b370', url: 'https://github.com/atom/github/pull/1684/commits/bad1deaea3d816383721478fc631b5edd0c2b370', }; From 7b8e564fdd763dca103548297badde0bebf3ae58 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 30 Nov 2018 20:12:17 +0100 Subject: [PATCH 114/117] use the long sha for opening commit item --- lib/views/pr-commit-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/pr-commit-view.js b/lib/views/pr-commit-view.js index ed7d865fc6e..e1fa7302799 100644 --- a/lib/views/pr-commit-view.js +++ b/lib/views/pr-commit-view.js @@ -42,7 +42,7 @@ export class PrCommitView extends React.Component { } openCommitDetailItem = () => { - return this.props.onBranch ? this.props.openCommit({sha: this.props.item.shortSha}) : null; + return this.props.onBranch ? this.props.openCommit({sha: this.props.item.sha}) : null; } render() { From d868567c4326071af6d0431e2cc528b7b9f25852 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 30 Nov 2018 20:42:03 +0100 Subject: [PATCH 115/117] clickable styling should only apply to clickable nodes! DUH! --- styles/pr-commit-view.less | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/styles/pr-commit-view.less b/styles/pr-commit-view.less index 0863199eecf..5847837599b 100644 --- a/styles/pr-commit-view.less +++ b/styles/pr-commit-view.less @@ -42,10 +42,11 @@ margin: 0 0 .25em 0; font-size: 1.2em; line-height: 1.4; - &:hover { - cursor: pointer; - text-decoration: underline; - } + } + + &-messageHeadline.clickable:hover { + cursor: pointer; + text-decoration: underline; } &-avatar { From 4cf2d295c0c508c259d73772795abafe09a8981e Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 30 Nov 2018 20:52:01 +0100 Subject: [PATCH 116/117] add tests for opening commits from PR view --- test/views/pr-commit-view.test.js | 34 ++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/test/views/pr-commit-view.test.js b/test/views/pr-commit-view.test.js index db9c453c95e..c98e88b4e5d 100644 --- a/test/views/pr-commit-view.test.js +++ b/test/views/pr-commit-view.test.js @@ -11,21 +11,25 @@ const defaultProps = { date: '2018-05-16T21:54:24.500Z', }, messageHeadline: 'This one weird trick for getting to the moon will blow your mind 🚀', - abbreviatedOid: 'bad1dea', + shortSha: 'bad1dea', + sha: 'bad1deaea3d816383721478fc631b5edd0c2b370', url: 'https://github.com/atom/github/pull/1684/commits/bad1deaea3d816383721478fc631b5edd0c2b370', }; -const getProps = function(overrides = {}) { +const getProps = function(itemOverrides = {}, overrides = {}) { return { item: { ...defaultProps, - ...overrides, + ...itemOverrides, }, + onBranch: true, + openCommit: () => {}, + ...overrides, }; }; describe('PrCommitView', function() { - function buildApp(overrideProps = {}) { - return ; + function buildApp(itemOverrides = {}, overrides = {}) { + return ; } it('renders the commit view for commits without message body', function() { const wrapper = shallow(buildApp({})); @@ -70,4 +74,24 @@ describe('PrCommitView', function() { assert.lengthOf(wrapper.find('.github-PrCommitView-moreText'), 0); assert.deepEqual(wrapper.find('.github-PrCommitView-moreButton').text(), 'show more...'); }); + + describe('if PR is checked out', function() { + it('shows message headlines as clickable', function() { + const wrapper = shallow(buildApp({})); + assert.isTrue(wrapper.find('.github-PrCommitView-messageHeadline').at(0).hasClass('clickable')); + }); + + it('opens a commit with the full sha when title is clicked', function() { + const openCommit = sinon.spy(); + const wrapper = shallow(buildApp({sha: 'longsha123'}, {openCommit})); + wrapper.find('.github-PrCommitView-messageHeadline').at(0).simulate('click'); + assert.isTrue(openCommit.calledWith({sha: 'longsha123'})); + }); + }); + + it('does not show message headlines as clickable if PR is not checked out', function() { + const wrapper = shallow(buildApp({}, {onBranch: false})); + assert.isFalse(wrapper.find('.github-PrCommitView-messageHeadline').at(0).hasClass('clickable')); + }); + }); From b720c7da8374d70ff239256d1b6877bb9e129594 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 30 Nov 2018 15:39:32 -0500 Subject: [PATCH 117/117] Incremental improvement to GitTab focus I've done a pass through the GitTab components and improved the focus management code somewhat. It's still ref soup, imperative, and pretty verbose, but at least now it's a bit more internally consistent. GitTab components that may receive focus implement four methods: * `getFocus(element)` returns the logical focus symbol corresponding to a DOM element, or null if the element is unrecognized. * `setFocus(symbol)` brings focus to the DOM element corresponding to a logical focus symbol. It returns true if an element was found and focused successfully and false otherwise. * `advanceFocusFrom(lastFocus)` returns a Promise that resolves to the logical focus symbol after a given symbol. * `retreatFocusFrom(lastFocus)` returns a Promise that resolves to the logical focus symbol before a given symbol. --- keymaps/git.cson | 1 + lib/controllers/commit-controller.js | 18 +- lib/controllers/git-tab-controller.js | 2 +- lib/controllers/recent-commits-controller.js | 12 +- lib/views/commit-view.js | 84 +++---- lib/views/git-tab-view.js | 80 +++---- lib/views/recent-commits-view.js | 25 +- lib/views/staging-view.js | 45 +++- test/controllers/commit-controller.test.js | 27 +-- .../recent-commits-controller.test.js | 16 +- test/views/commit-view.test.js | 215 ++++++++---------- test/views/git-tab-view.test.js | 99 +++++--- test/views/recent-commits-view.test.js | 28 +++ test/views/staging-view.test.js | 130 +++++++---- 14 files changed, 441 insertions(+), 341 deletions(-) diff --git a/keymaps/git.cson b/keymaps/git.cson index 16f31c82d63..d7b6d2c1d82 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -39,6 +39,7 @@ '.github-CommitView-editor atom-text-editor:not([mini])': 'cmd-enter': 'github:commit' 'ctrl-enter': 'github:commit' + 'tab': 'core:focus-next' 'shift-tab': 'core:focus-previous' '.github-CommitView-commitPreview': diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index b07be5d280a..7183eeb31a3 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -76,7 +76,7 @@ export default class CommitController extends React.Component { }), this.props.workspace.onDidDestroyPaneItem(async ({item}) => { if (this.props.repository.isPresent() && item.getPath && item.getPath() === this.getCommitMessagePath() && - this.getCommitMessageEditors().length === 0) { + this.getCommitMessageEditors().length === 0) { // we closed the last editor pointing to the commit message file try { this.commitMessageBuffer.setText(await fs.readFile(this.getCommitMessagePath(), {encoding: 'utf8'})); @@ -252,24 +252,20 @@ export default class CommitController extends React.Component { this.grammarSubscription.dispose(); } - rememberFocus(event) { - return this.refCommitView.map(view => view.rememberFocus(event)).getOr(null); + getFocus(element) { + return this.refCommitView.map(view => view.getFocus(element)).getOr(null); } setFocus(focus) { return this.refCommitView.map(view => view.setFocus(focus)).getOr(false); } - advanceFocus(...args) { - return this.refCommitView.map(view => view.advanceFocus(...args)).getOr(false); + advanceFocusFrom(...args) { + return this.refCommitView.map(view => view.advanceFocusFrom(...args)).getOr(false); } - retreatFocus(...args) { - return this.refCommitView.map(view => view.retreatFocus(...args)).getOr(false); - } - - hasFocusAtBeginning() { - return this.refCommitView.map(view => view.hasFocusAtBeginning()).getOr(false); + retreatFocusFrom(...args) { + return this.refCommitView.map(view => view.retreatFocusFrom(...args)).getOr(false); } toggleCommitPreview() { diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index c1d732df4fc..f70da000dce 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 { } rememberLastFocus(event) { - this.lastFocus = this.refView.map(view => view.rememberFocus(event)).getOr(null) || GitTabView.focus.STAGING; + this.lastFocus = this.refView.map(view => view.getFocus(event.target)).getOr(null) || GitTabView.focus.STAGING; } restoreFocus() { diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index ac4b6d97435..2967983062d 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -94,11 +94,19 @@ export default class RecentCommitsController extends React.Component { } } - rememberFocus(event) { - return this.refView.map(view => view.rememberFocus(event)).getOr(null); + getFocus(element) { + return this.refView.map(view => view.getFocus(element)).getOr(null); } setFocus(focus) { return this.refView.map(view => view.setFocus(focus)).getOr(false); } + + advanceFocusFrom(focus) { + return this.refView.map(view => view.advanceFocusFrom(focus)).getOr(Promise.resolve(false)); + } + + retreatFocusFrom(focus) { + return this.refView.map(view => view.retreatFocusFrom(focus)).getOr(Promise.resolve(false)); + } } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 6013162cfaf..fc35d537042 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -7,6 +7,8 @@ import Select from 'react-select'; import Tooltip from '../atom/tooltip'; import AtomTextEditor from '../atom/atom-text-editor'; import CoAuthorForm from './co-author-form'; +import RecentCommitsView from './recent-commits-view'; +import StagingView from './staging-view'; import Commands, {Command} from '../atom/commands'; import RefHolder from '../models/ref-holder'; import Author from '../models/author'; @@ -30,6 +32,10 @@ export default class CommitView extends React.Component { COMMIT_BUTTON: Symbol('commit-button'), }; + static firstFocus = CommitView.focus.COMMIT_PREVIEW_BUTTON; + + static lastFocus = CommitView.focus.COMMIT_BUTTON; + static propTypes = { workspace: PropTypes.object.isRequired, config: PropTypes.object.isRequired, @@ -569,15 +575,7 @@ export default class CommitView extends React.Component { return this.refRoot.map(element => element.contains(document.activeElement)).getOr(false); } - hasFocusEditor() { - return this.refEditorComponent.map(editor => editor.contains(document.activeElement)).getOr(false); - } - - hasFocusAtBeginning() { - return this.refCommitPreviewButton.map(button => button.contains(document.activeElement)).getOr(false); - } - - getFocus(element = document.activeElement) { + getFocus(element) { if (this.refCommitPreviewButton.map(button => button.contains(element)).getOr(false)) { return CommitView.focus.COMMIT_PREVIEW_BUTTON; } @@ -601,10 +599,6 @@ export default class CommitView extends React.Component { return null; } - rememberFocus(event) { - return this.getFocus(event.target); - } - setFocus(focus) { let fallback = false; const focusElement = element => { @@ -657,19 +651,23 @@ export default class CommitView extends React.Component { return false; } - advanceFocus(event) { + advanceFocusFrom(focus) { const f = this.constructor.focus; - const current = this.getFocus(); - if (current === f.EDITOR) { - // Let the editor handle it - return true; - } let next = null; - switch (current) { + switch (focus) { case f.COMMIT_PREVIEW_BUTTON: next = f.EDITOR; break; + case f.EDITOR: + if (this.state.showCoAuthorInput) { + next = f.COAUTHOR_INPUT; + } else if (this.props.isMerging) { + next = f.ABORT_MERGE_BUTTON; + } else { + next = f.COMMIT_BUTTON; + } + break; case f.COAUTHOR_INPUT: next = this.props.isMerging ? f.ABORT_MERGE_BUTTON : f.COMMIT_BUTTON; break; @@ -677,57 +675,41 @@ export default class CommitView extends React.Component { next = f.COMMIT_BUTTON; break; case f.COMMIT_BUTTON: - // End of tab navigation. Prevent cycling. - event.stopPropagation(); - return true; + next = RecentCommitsView.firstFocus; + break; } - if (next !== null) { - this.setFocus(next); - event.stopPropagation(); - - return true; - } else { - return false; - } + return Promise.resolve(next); } - retreatFocus(event) { + retreatFocusFrom(focus) { const f = this.constructor.focus; - const current = this.getFocus(); - let next = null; - switch (current) { + let previous = null; + switch (focus) { case f.COMMIT_BUTTON: if (this.props.isMerging) { - next = f.ABORT_MERGE_BUTTON; + previous = f.ABORT_MERGE_BUTTON; } else if (this.state.showCoAuthorInput) { - next = f.COAUTHOR_INPUT; + previous = f.COAUTHOR_INPUT; } else { - next = f.EDITOR; + previous = f.EDITOR; } break; case f.ABORT_MERGE_BUTTON: - next = this.state.showCoAuthorInput ? f.COAUTHOR_INPUT : f.EDITOR; + previous = this.state.showCoAuthorInput ? f.COAUTHOR_INPUT : f.EDITOR; break; case f.COAUTHOR_INPUT: - next = f.EDITOR; + previous = f.EDITOR; break; case f.EDITOR: - next = f.COMMIT_PREVIEW_BUTTON; + previous = f.COMMIT_PREVIEW_BUTTON; break; case f.COMMIT_PREVIEW_BUTTON: - // Allow the GitTabView to retreat focus back to the last StagingView list. - return false; + previous = StagingView.lastFocus; + break; } - if (next !== null) { - this.setFocus(next); - event.stopPropagation(); - - return true; - } else { - return false; - } + return Promise.resolve(previous); } } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 1330e299c0e..4ec74fde668 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -224,35 +224,22 @@ export default class GitTabView extends React.Component { this.props.initializeRepo(initPath); } - rememberFocus(event) { - let currentFocus = null; - - currentFocus = this.props.refStagingView.map(view => view.rememberFocus(event)).getOr(null); - - if (!currentFocus) { - currentFocus = this.refCommitController.map(controller => controller.rememberFocus(event)).getOr(null); - } - - if (!currentFocus) { - currentFocus = this.refRecentCommitsController.map(controller => controller.rememberFocus(event)).getOr(null); + getFocus(element) { + for (const ref of [this.props.refStagingView, this.refCommitController, this.refRecentCommitsController]) { + const focus = ref.map(sub => sub.getFocus(element)).getOr(null); + if (focus !== null) { + return focus; + } } - - return currentFocus; + return null; } setFocus(focus) { - if (this.props.refStagingView.map(view => view.setFocus(focus)).getOr(false)) { - return true; - } - - if (this.refCommitController.map(controller => controller.setFocus(focus)).getOr(false)) { - return true; - } - - if (this.refRecentCommitsController.map(controller => controller.setFocus(focus)).getOr(false)) { - return true; + for (const ref of [this.props.refStagingView, this.refCommitController, this.refRecentCommitsController]) { + if (ref.map(sub => sub.setFocus(focus)).getOr(false)) { + return true; + } } - return false; } @@ -261,39 +248,34 @@ export default class GitTabView extends React.Component { } async advanceFocus(evt) { - // Advance focus within the CommitView if it's there - if (this.refCommitController.map(c => c.advanceFocus(evt)).getOr(false)) { - return; - } + const currentFocus = this.getFocus(document.activeElement); + let nextSeen = false; - // Advance focus to the next staging view list, if it's there - if (await this.props.refStagingView.map(view => view.activateNextList()).getOr(false)) { - evt.stopPropagation(); - return; - } - - // Advance focus from the staging view lists to the CommitView - if (this.refCommitController.map(c => c.setFocus(GitTabView.focus.COMMIT_PREVIEW_BUTTON)).getOr(false)) { - evt.stopPropagation(); + for (const subHolder of [this.props.refStagingView, this.refCommitController, this.refRecentCommitsController]) { + const next = await subHolder.map(sub => sub.advanceFocusFrom(currentFocus)).getOr(null); + if (next !== null && !nextSeen) { + nextSeen = true; + evt.stopPropagation(); + if (next !== currentFocus) { + this.setFocus(next); + } + } } } async retreatFocus(evt) { - // Retreat focus within the CommitView if it's there - if (this.refCommitController.map(c => c.retreatFocus(evt)).getOr(false)) { - return; - } + const currentFocus = this.getFocus(document.activeElement); + let previousSeen = false; - if (this.refCommitController.map(c => c.hasFocusAtBeginning()).getOr(false)) { - // Retreat focus from the beginning of the CommitView to the end of the StagingView - if (await this.props.refStagingView.map(view => view.activateLastList()).getOr(null)) { - this.setFocus(GitTabView.focus.STAGING); + for (const subHolder of [this.refRecentCommitsController, this.refCommitController, this.props.refStagingView]) { + const previous = await subHolder.map(sub => sub.retreatFocusFrom(currentFocus)).getOr(null); + if (previous !== null && !previousSeen) { + previousSeen = true; evt.stopPropagation(); + if (previous !== currentFocus) { + this.setFocus(previous); + } } - } else if (await this.props.refStagingView.map(c => c.activatePreviousList()).getOr(null)) { - // Retreat focus within the StagingView - this.setFocus(GitTabView.focus.STAGING); - evt.stopPropagation(); } } diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index dfd60105fc2..ac64db3d404 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -7,6 +7,7 @@ import {emojify} from 'node-emoji'; import Commands, {Command} from '../atom/commands'; import RefHolder from '../models/ref-holder'; +import CommitView from './commit-view'; import Timeago from './timeago'; class RecentCommitView extends React.Component { @@ -130,6 +131,10 @@ export default class RecentCommitsView extends React.Component { RECENT_COMMIT: Symbol('recent_commit'), }; + static firstFocus = RecentCommitsView.focus.RECENT_COMMIT; + + static lastFocus = RecentCommitsView.focus.RECENT_COMMIT; + constructor(props) { super(props); this.refRoot = new RefHolder(); @@ -143,8 +148,8 @@ export default class RecentCommitsView extends React.Component { return false; } - rememberFocus(event) { - return this.refRoot.map(element => element.contains(event.target)).getOr(false) + getFocus(element) { + return this.refRoot.map(e => e.contains(element)).getOr(false) ? this.constructor.focus.RECENT_COMMIT : null; } @@ -198,4 +203,20 @@ export default class RecentCommitsView extends React.Component { } openSelectedCommit = () => this.props.openCommit({sha: this.props.selectedCommitSha, preserveFocus: false}) + + advanceFocusFrom(focus) { + if (focus === this.constructor.focus.RECENT_COMMIT) { + return Promise.resolve(this.constructor.focus.RECENT_COMMIT); + } + + return Promise.resolve(null); + } + + retreatFocusFrom(focus) { + if (focus === this.constructor.focus.RECENT_COMMIT) { + return Promise.resolve(CommitView.lastFocus); + } + + return Promise.resolve(null); + } } diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 8629ff89534..474db1aaa0c 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -12,6 +12,7 @@ import ObserveModel from './observe-model'; import MergeConflictListItemView from './merge-conflict-list-item-view'; import CompositeListSelection from '../models/composite-list-selection'; import ResolutionProgress from '../models/conflicts/resolution-progress'; +import CommitView from './commit-view'; import RefHolder from '../models/ref-holder'; import ChangedFileItem from '../items/changed-file-item'; import Commands, {Command} from '../atom/commands'; @@ -43,7 +44,7 @@ function calculateTruncatedLists(lists) { }, {source: {}}); } -const noop = () => {}; +const noop = () => { }; const MAXIMUM_LISTED_ENTRIES = 1000; @@ -76,6 +77,10 @@ export default class StagingView extends React.Component { STAGING: Symbol('staging'), }; + static firstFocus = StagingView.focus.STAGING; + + static lastFocus = StagingView.focus.STAGING; + constructor(props) { super(props); autobind( @@ -226,12 +231,12 @@ export default class StagingView extends React.Component {
    {this.renderTruncatedMessage(this.props.unstagedChanges)}
    - { this.renderMergeConflicts() } + {this.renderMergeConflicts()}
    - Staged Changes + Staged Changes