From 5abde1f2893812b67b5fe0123964b2a519dbd2c1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Nov 2018 12:43:00 -0800 Subject: [PATCH 01/87] 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 02/87] 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 03/87] 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 04/87] 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 05/87] 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 06/87] 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 07/87] 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 08/87] 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 09/87] 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 10/87] 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 11/87] 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 12/87] 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 13/87] 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 14/87] 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 15/87] 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 16/87] 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 17/87] 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 18/87] 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 19/87] 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 20/87] 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 36/87] 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 37/87] 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 38/87] :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 39/87] 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 40/87] 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 41/87] 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 42/87] 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 43/87] 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 44/87] 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 45/87] 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 46/87] 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 47/87] 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 48/87] 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 49/87] 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 50/87] 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 51/87] :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 52/87] 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 53/87] 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 54/87] 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 55/87] :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 56/87] 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 57/87] :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 58/87] 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 59/87] 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 60/87] 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 61/87] 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 62/87] 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 63/87] 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 64/87] `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 65/87] 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 66/87] [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 67/87] 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 68/87] 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 69/87] 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 70/87] 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 71/87] 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 72/87] `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 73/87] 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 74/87] 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 75/87] 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 76/87] 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 77/87] 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 78/87] 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 79/87] 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 80/87] 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 81/87] 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 82/87] 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 83/87] 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 84/87] 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 85/87] 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 86/87] 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 87/87] [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); + }); });