{this.props.children}
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 9899daca097..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,9 +195,12 @@ export default class GitTabView extends React.Component {
updateSelectedCoAuthors={this.props.updateSelectedCoAuthors}
/>
);
@@ -289,6 +294,10 @@ export default class GitTabView extends React.Component {
this.setFocus(GitTabView.focus.STAGING);
}
+ focusAndSelectRecentCommit() {
+ this.setFocus(RecentCommitsView.focus.RECENT_COMMIT);
+ }
+
focusAndSelectCommitPreviewButton() {
this.setFocus(GitTabView.focus.COMMIT_PREVIEW_BUTTON);
}
diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js
index cb72feb36ac..a2ed357015f 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();
@@ -17,22 +20,23 @@ 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,
+ itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired,
};
constructor(props) {
super(props);
- autobind(this, 'didMouseDown');
+ autobind(this, 'didMouseDown', 'renderButtons');
this.refDiscardButton = new RefHolder();
}
@@ -48,32 +52,44 @@ export default class HunkHeaderView extends React.Component {
{this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()}
-
- {this.props.stagingStatus === 'unstaged' && (
-
-
-
-
- )}
+ {this.renderButtons()}
);
}
+ renderButtons() {
+ if (this.props.itemType === CommitDetailItem) {
+ return null;
+ } else {
+ return (
+
+
+ {this.props.stagingStatus === 'unstaged' && (
+
+
+
+
+ )}
+
+ );
+ }
+ }
+
didMouseDown(event) {
return this.props.mouseDown(event, this.props.hunk);
}
diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js
index 0ff63f97b39..5d1407e4963 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,20 +47,19 @@ export default class MultiFilePatchView extends React.Component {
tooltips: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
- selectedRowsChanged: PropTypes.func.isRequired,
-
- 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,
-
+ selectedRowsChanged: PropTypes.func,
+
+ 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,
+ itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem, CommitDetailItem]).isRequired,
}
constructor(props) {
@@ -186,6 +186,15 @@ export default class MultiFilePatchView extends React.Component {
}
renderCommands() {
+ if (this.props.itemType === CommitDetailItem) {
+ return (
+
+
+
+
+ );
+ }
+
let stageModeCommand = null;
let stageSymlinkCommand = null;
@@ -205,11 +214,11 @@ export default class MultiFilePatchView extends React.Component {
return (
+
+
-
-
@@ -352,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
@@ -435,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}
@@ -493,6 +504,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}
/>
diff --git a/lib/views/open-commit-dialog.js b/lib/views/open-commit-dialog.js
new file mode 100644
index 00000000000..47ab2d3ec21
--- /dev/null
+++ b/lib/views/open-commit-dialog.js
@@ -0,0 +1,138 @@
+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');
+
+ 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/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js
index 855fad6515a..9faffd716df 100644
--- a/lib/views/recent-commits-view.js
+++ b/lib/views/recent-commits-view.js
@@ -4,6 +4,9 @@ import moment from 'moment';
import cx from 'classnames';
import {emojify} from 'node-emoji';
+import Commands, {Command} from '../atom/commands';
+import RefHolder from '../models/ref-holder';
+
import Timeago from './timeago';
class RecentCommitView extends React.Component {
@@ -11,6 +14,8 @@ class RecentCommitView extends React.Component {
commit: PropTypes.object.isRequired,
undoLastCommit: PropTypes.func.isRequired,
isMostRecent: PropTypes.bool.isRequired,
+ openCommit: PropTypes.func.isRequired,
+ isSelected: PropTypes.bool.isRequired,
};
render() {
@@ -18,7 +23,12 @@ class RecentCommitView extends React.Component {
const fullMessage = this.props.commit.getFullMessage();
return (
-
+
{this.renderAuthors()}
+ onClick={this.undoLastCommit}>
Undo
)}
@@ -73,6 +83,11 @@ class RecentCommitView extends React.Component {
);
}
+
+ undoLastCommit = event => {
+ event.stopPropagation();
+ this.props.undoLastCommit();
+ }
}
export default class RecentCommitsView extends React.Component {
@@ -80,11 +95,39 @@ export default class RecentCommitsView extends React.Component {
commits: PropTypes.arrayOf(PropTypes.object).isRequired,
isLoading: PropTypes.bool.isRequired,
undoLastCommit: PropTypes.func.isRequired,
+ openCommit: PropTypes.func.isRequired,
+ selectedCommitSha: PropTypes.string.isRequired,
+ commandRegistry: PropTypes.object.isRequired,
};
+ static focus = {
+ RECENT_COMMIT: Symbol('recent_commit'),
+ };
+
+ constructor(props) {
+ super(props);
+ this.refRecentCommits = new RefHolder();
+ }
+
+ setFocus(focus) {
+ return this.refRecentCommits.map(view => 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()}
);
@@ -115,6 +158,8 @@ export default class RecentCommitsView extends React.Component {
isMostRecent={i === 0}
commit={commit}
undoLastCommit={this.props.undoLastCommit}
+ openCommit={() => this.props.openCommit({sha: commit.getSha()})}
+ isSelected={this.props.selectedCommitSha === commit.getSha()}
/>
);
})}
diff --git a/package.json b/package.json
index 94ea85116db..e8a5fed2e37 100644
--- a/package.json
+++ b/package.json
@@ -203,6 +203,7 @@
"GitDockItem": "createDockItemStub",
"GithubDockItem": "createDockItemStub",
"FilePatchControllerStub": "createFilePatchControllerStub",
- "CommitPreviewStub": "createCommitPreviewStub"
+ "CommitPreviewStub": "createCommitPreviewStub",
+ "CommitDetailStub": "createCommitDetailStub"
}
}
diff --git a/styles/commit-detail.less b/styles/commit-detail.less
new file mode 100644
index 00000000000..9f9bcc47620
--- /dev/null
+++ b/styles/commit-detail.less
@@ -0,0 +1,94 @@
+@import "variables";
+
+@default-padding: @component-padding;
+@avatar-dimensions: 16px;
+
+.github-CommitDetailView {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ &-header {
+ flex: 0;
+ border-bottom: 1px solid @base-border-color;
+ background-color: @syntax-background-color;
+ }
+
+ &-commit {
+ padding: @default-padding*2;
+ padding-bottom: 0;
+ }
+
+ &-title {
+ margin: 0 0 .25em 0;
+ font-size: 1.4em;
+ line-height: 1.3;
+ color: @text-color-highlight;
+ }
+
+ &-avatar {
+ border-radius: @component-border-radius;
+ height: @avatar-dimensions;
+ margin-right: .4em;
+ width: @avatar-dimensions;
+ }
+
+ &-meta {
+ display: flex;
+ align-items: center;
+ margin: @default-padding/2 0 @default-padding*2 0;
+ }
+
+ &-metaText {
+ flex: 1;
+ margin-left: @avatar-dimensions * 1.3; // leave some space for the avatars
+ line-height: @avatar-dimensions;
+ color: @text-color-subtle;
+ }
+
+ &-moreButton {
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ padding: 0em .4em;
+ color: @text-color-subtle;
+ font-style: italic;
+ border: 1px solid @base-border-color;
+ border-radius: @component-border-radius;
+ background-color: @button-background-color;
+
+ &:hover {
+ background-color: @button-background-color-hover
+ }
+ }
+
+ &-moreText {
+ 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.
+ max-height: 55vh;
+ &:empty {
+ display: none;
+ }
+ }
+
+ &-sha {
+ flex: 0 0 7ch; // Limit to 7 characters
+ margin-left: @default-padding*2;
+ line-height: @avatar-dimensions;
+ color: @text-color-subtle;
+ font-family: var(--editor-font-family);
+ white-space: nowrap;
+ overflow: hidden;
+ a {
+ color: @text-color-info;
+ }
+ }
+}
diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less
index 3de411746a3..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 {
@@ -32,14 +32,6 @@
}
}
- // Editor overrides
-
- atom-text-editor {
- .selection .region {
- background-color: mix(@button-background-color-selected, @syntax-background-color, 24%);
- }
- }
-
&-header {
display: flex;
justify-content: space-between;
@@ -220,11 +212,6 @@
min-width: 6ch; // Fit up to 4 characters (+1 padding on each side)
opacity: 1;
padding: 0 1ch 0 0;
-
- &.github-FilePatchView-line--selected {
- color: contrast(@button-background-color-selected);
- background: @button-background-color-selected;
- }
}
&.icons .line-number {
@@ -249,11 +236,6 @@
&.github-FilePatchView-line--nonewline:before {
content: @no-newline;
}
-
- &.github-FilePatchView-line--selected {
- color: contrast(@button-background-color-selected);
- background: @button-background-color-selected;
- }
}
}
@@ -272,6 +254,49 @@
}
}
+
+// States
+
+// Selected
+.github-FilePatchView {
+ .gutter {
+ &.old .line-number,
+ &.new .line-number,
+ &.icons .line-number {
+ &.github-FilePatchView-line--selected {
+ color: @text-color-selected;
+ background: @background-color-selected;
+ }
+ }
+ }
+
+ atom-text-editor {
+ .selection .region {
+ background-color: transparent;
+ }
+ }
+}
+
+// Selected + focused
+.github-FilePatchView:focus-within {
+ .gutter {
+ &.old .line-number,
+ &.new .line-number,
+ &.icons .line-number {
+ &.github-FilePatchView-line--selected {
+ color: contrast(@button-background-color-selected);
+ background: @button-background-color-selected;
+ }
+ }
+ }
+
+ atom-text-editor {
+ .selection .region {
+ background-color: mix(@button-background-color-selected, @syntax-background-color, 24%);
+ }
+ }
+}
+
.gitub-FilePatchHeaderView-basename {
font-weight: bold;
}
diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less
index 11cffdec766..5d4bfa61de0 100644
--- a/styles/hunk-header-view.less
+++ b/styles/hunk-header-view.less
@@ -68,9 +68,10 @@
}
}
+// Selected
.github-HunkHeaderView--isSelected {
- color: contrast(@button-background-color-selected);
- background-color: @button-background-color-selected;
+ color: @text-color-selected;
+ background-color: @background-color-selected;
border-color: transparent;
.github-HunkHeaderView-title {
color: inherit;
@@ -78,7 +79,22 @@
.github-HunkHeaderView-title,
.github-HunkHeaderView-stageButton,
.github-HunkHeaderView-discardButton {
- &:hover { background-color: lighten(@button-background-color-selected, 4%); }
- &:active { background-color: darken(@button-background-color-selected, 4%); }
+ &:hover { background-color: @background-color-highlight; }
+ &:active { background-color: @background-color-selected; }
+ }
+}
+
+
+// Selected + focused
+.github-FilePatchView:focus-within {
+ .github-HunkHeaderView--isSelected {
+ color: contrast(@button-background-color-selected);
+ background-color: @button-background-color-selected;
+ .github-HunkHeaderView-title,
+ .github-HunkHeaderView-stageButton,
+ .github-HunkHeaderView-discardButton {
+ &:hover { background-color: lighten(@button-background-color-selected, 4%); }
+ &:active { background-color: darken(@button-background-color-selected, 4%); }
+ }
}
}
diff --git a/styles/recent-commits.less b/styles/recent-commits.less
index 06a3bda3b56..f9ad97ef664 100644
--- a/styles/recent-commits.less
+++ b/styles/recent-commits.less
@@ -87,6 +87,17 @@
color: @text-color-subtle;
}
+ &:hover {
+ color: @text-color-highlight;
+ background: @background-color-highlight;
+ }
+
+ &.is-selected {
+ // is selected
+ color: @text-color-selected;
+ background: @background-color-selected;
+ }
+
}
diff --git a/test/builder/commit.js b/test/builder/commit.js
new file mode 100644
index 00000000000..d9dd960bb77
--- /dev/null
+++ b/test/builder/commit.js
@@ -0,0 +1,75 @@
+import moment from 'moment';
+
+import Commit from '../../lib/models/commit';
+import {multiFilePatchBuilder} from './patch';
+
+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';
+
+ this._multiFileDiff = null;
+ }
+
+ 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;
+ }
+
+ setMultiFileDiff(block = () => {}) {
+ const builder = multiFilePatchBuilder();
+ block(builder);
+ 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,
+ authorEmail: this._authorEmail,
+ authorDate: this._authorDate,
+ coAuthors: this._coAuthors,
+ messageSubject: this._messageSubject,
+ messageBody: this._messageBody,
+ });
+
+ if (this._multiFileDiff !== null) {
+ commit.setMultiFileDiff(this._multiFileDiff);
+ }
+
+ return commit;
+ }
+}
+
+export function commitBuilder() {
+ return new CommitBuilder();
+}
diff --git a/test/containers/commit-detail-container.test.js b/test/containers/commit-detail-container.test.js
new file mode 100644
index 00000000000..e4dff8718fc
--- /dev/null
+++ b/test/containers/commit-detail-container.test.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import {mount} from 'enzyme';
+
+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;
+
+ beforeEach(async function() {
+ atomEnv = global.buildAtomEnvironment();
+
+ const workdir = await cloneRepository('multiple-commits');
+ repository = await buildRepository(workdir);
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ });
+
+ function buildApp(override = {}) {
+
+ const props = {
+ repository,
+ sha: VALID_SHA,
+
+ itemType: CommitDetailItem,
+ workspace: atomEnv.workspace,
+ commands: atomEnv.commands,
+ keymaps: atomEnv.keymaps,
+ tooltips: atomEnv.tooltips,
+ config: atomEnv.config,
+
+ destroy: () => {},
+
+ ...override,
+ };
+
+ return ;
+ }
+
+ it('renders a loading spinner while the repository is loading', function() {
+ const wrapper = mount(buildApp());
+ assert.isTrue(wrapper.find('LoadingView').exists());
+ });
+
+ it('renders a loading spinner while the file patch is being loaded', async function() {
+ await repository.getLoadPromise();
+ const commitPromise = repository.getCommit(VALID_SHA);
+ let resolveDelayedPromise = () => {};
+ const delayedPromise = new Promise(resolve => {
+ resolveDelayedPromise = resolve;
+ });
+ sinon.stub(repository, 'getCommit').returns(delayedPromise);
+
+ const wrapper = mount(buildApp());
+
+ assert.isTrue(wrapper.find('LoadingView').exists());
+ 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(VALID_SHA);
+
+ const wrapper = mount(buildApp());
+ await assert.async.isTrue(wrapper.update().find('CommitDetailController').exists());
+ assert.strictEqual(wrapper.find('CommitDetailController').prop('commit'), commit);
+ });
+});
diff --git a/test/controllers/commit-detail-controller.test.js b/test/controllers/commit-detail-controller.test.js
new file mode 100644
index 00000000000..2231af47da2
--- /dev/null
+++ b/test/controllers/commit-detail-controller.test.js
@@ -0,0 +1,107 @@
+import React from 'react';
+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';
+
+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(VALID_SHA);
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ });
+
+ function buildApp(override = {}) {
+ const props = {
+ repository,
+ commit,
+ itemType: CommitDetailItem,
+
+ workspace: atomEnv.workspace,
+ commands: atomEnv.commands,
+ keymaps: atomEnv.keymaps,
+ tooltips: atomEnv.tooltips,
+ config: atomEnv.config,
+ destroy: () => {},
+
+ ...override,
+ };
+
+ return ;
+ }
+
+ 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 its CommitDetailView', function() {
+ const extra = Symbol('extra');
+ const wrapper = shallow(buildApp({extra}));
+ assert.strictEqual(wrapper.find('CommitDetailView').prop('extra'), extra);
+ });
+
+ 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'));
+ });
+
+ 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'));
+ });
+
+ 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/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() {
diff --git a/test/items/commit-detail-item.test.js b/test/items/commit-detail-item.test.js
new file mode 100644
index 00000000000..eecbc66cf6a
--- /dev/null
+++ b/test/items/commit-detail-item.test.js
@@ -0,0 +1,168 @@
+import React from 'react';
+import {mount} from 'enzyme';
+
+import CommitDetailItem from '../../lib/items/commit-detail-item';
+import PaneItem from '../../lib/atom/pane-item';
+import WorkdirContextPool from '../../lib/models/workdir-context-pool';
+import {cloneRepository} from '../helpers';
+
+describe('CommitDetailItem', function() {
+ let atomEnv, repository, pool;
+
+ beforeEach(async function() {
+ atomEnv = global.buildAtomEnvironment();
+ const workdir = await cloneRepository('multiple-commits');
+
+ pool = new WorkdirContextPool({
+ workspace: atomEnv.workspace,
+ });
+
+ repository = pool.add(workdir).getRepository();
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ pool.clear();
+ });
+
+ function buildPaneApp(override = {}) {
+ const props = {
+ workdirContextPool: pool,
+ workspace: atomEnv.workspace,
+ commands: atomEnv.commands,
+ keymaps: atomEnv.keymaps,
+ tooltips: atomEnv.tooltips,
+ config: atomEnv.config,
+ discardLines: () => {},
+ ...override,
+ };
+
+ return (
+
+ {({itemHolder, params}) => {
+ return (
+
+ );
+ }}
+
+ );
+ }
+
+ function open(wrapper, options = {}) {
+ const opts = {
+ workingDirectory: repository.getWorkingDirectoryPath(),
+ sha: '18920c900bfa6e4844853e7e246607a31c3e2e8c',
+ ...options,
+ };
+ const uri = CommitDetailItem.buildURI(opts.workingDirectory, opts.sha);
+ return atomEnv.workspace.open(uri);
+ }
+
+ it('constructs and opens the correct URI', async function() {
+ const wrapper = mount(buildPaneApp());
+ await open(wrapper);
+
+ assert.isTrue(wrapper.update().find('CommitDetailItem').exists());
+ });
+
+ it('passes extra props to its container', async function() {
+ const extra = Symbol('extra');
+ const wrapper = mount(buildPaneApp({extra}));
+ await open(wrapper);
+
+ assert.strictEqual(wrapper.update().find('CommitDetailItem').prop('extra'), extra);
+ });
+
+ it('serializes itself as a CommitDetailItem', async function() {
+ const wrapper = mount(buildPaneApp());
+ const item0 = await open(wrapper, {workingDirectory: '/dir0', sha: '420'});
+ assert.deepEqual(item0.serialize(), {
+ deserializer: 'CommitDetailStub',
+ uri: 'atom-github://commit-detail?workdir=%2Fdir0&sha=420',
+ });
+
+ const item1 = await open(wrapper, {workingDirectory: '/dir1', sha: '1337'});
+ assert.deepEqual(item1.serialize(), {
+ deserializer: 'CommitDetailStub',
+ uri: 'atom-github://commit-detail?workdir=%2Fdir1&sha=1337',
+ });
+ });
+
+ it('locates the repository from the context pool', async function() {
+ const wrapper = mount(buildPaneApp());
+ await open(wrapper);
+
+ assert.strictEqual(wrapper.update().find('CommitDetailContainer').prop('repository'), repository);
+ });
+
+ it('passes an absent repository if the working directory is unrecognized', async function() {
+ const wrapper = mount(buildPaneApp());
+ await open(wrapper, {workingDirectory: '/nah'});
+
+ assert.isTrue(wrapper.update().find('CommitDetailContainer').prop('repository').isAbsent());
+ });
+
+ it('returns a fixed title and icon', async function() {
+ const wrapper = mount(buildPaneApp());
+ const item = await open(wrapper, {sha: '1337'});
+
+ assert.strictEqual(item.getTitle(), 'Commit: 1337');
+ assert.strictEqual(item.getIconName(), 'git-commit');
+ });
+
+ it('terminates pending state', async function() {
+ const wrapper = mount(buildPaneApp());
+
+ const item = await open(wrapper);
+ const callback = sinon.spy();
+ const sub = item.onDidTerminatePendingState(callback);
+
+ assert.strictEqual(callback.callCount, 0);
+ item.terminatePendingState();
+ assert.strictEqual(callback.callCount, 1);
+ item.terminatePendingState();
+ assert.strictEqual(callback.callCount, 1);
+
+ sub.dispose();
+ });
+
+ it('may be destroyed once', async function() {
+ const wrapper = mount(buildPaneApp());
+
+ const item = await open(wrapper);
+ const callback = sinon.spy();
+ const sub = item.onDidDestroy(callback);
+
+ assert.strictEqual(callback.callCount, 0);
+ item.destroy();
+ assert.strictEqual(callback.callCount, 1);
+
+ sub.dispose();
+ });
+
+ it('has an item-level accessor for the current working directory & sha', async function() {
+ const wrapper = mount(buildPaneApp());
+ const item = await open(wrapper, {workingDirectory: '/dir7', sha: '420'});
+ assert.strictEqual(item.getWorkingDirectory(), '/dir7');
+ 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);
+ });
+});
diff --git a/test/models/commit.test.js b/test/models/commit.test.js
new file mode 100644
index 00000000000..4b4d6aa08d8
--- /dev/null
+++ b/test/models/commit.test.js
@@ -0,0 +1,151 @@
+import dedent from 'dedent-js';
+
+import {nullCommit} from '../../lib/models/commit';
+import {commitBuilder} from '../builder/commit';
+
+describe('Commit', function() {
+ describe('isBodyLong()', function() {
+ it('returns false if the commit message body is short', function() {
+ const commit = commitBuilder().messageBody('short').build();
+ assert.isFalse(commit.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.
+ `;
+ const commit = commitBuilder().messageBody(messageBody).build();
+ 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());
+ });
+ });
+
+ 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 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. 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 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.
+ `;
+
+ 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.
+
+ ...
+ `);
+ });
+
+ 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.
+ `;
+
+ 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...
+ `);
+ });
+
+ it('truncates the message body at the character cutoff if no word or paragraph boundaries can be found', function() {
+ // 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,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.';
+
+ const commit = commitBuilder().messageBody(body).build();
+ assert.strictEqual(
+ commit.abbreviatedBody(),
+ 'Loremipsumdolorsitamet,ethisjustodeleniti,omniumfastidiiadversariumathas.' +
+ 'Mazimalterumseaea,essentmalorumpersiusnemei.Nameatemporqualisque,modusdomingtehas.Affertdolore' +
+ 'albuciustevis,eamtantasnullamcorrumpitad,inoratioluptatumeleifendvim.Easalutatuscontentioneseos.' +
+ 'Eaminveniamfacetevolutpat,solumappetereadversariumutquo.Velcuappetereurbanitas,usuutaperiri' +
+ 'mediocritatem,aliamolestieurbanitascuqui.Velitantiopamerroribusnoeum,scriptaiudicabitnenam,in' +
+ '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...');
+ });
+ });
+});
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index 834567e9ca8..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]());
}
@@ -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');
diff --git a/test/views/commit-detail-view.test.js b/test/views/commit-detail-view.test.js
new file mode 100644
index 00000000000..acca9a093d8
--- /dev/null
+++ b/test/views/commit-detail-view.test.js
@@ -0,0 +1,174 @@
+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';
+import Commit from '../../lib/models/commit';
+import {cloneRepository, buildRepository} from '../helpers';
+import {commitBuilder} from '../builder/commit';
+
+describe('CommitDetailView', function() {
+ let repository, atomEnv;
+
+ beforeEach(async function() {
+ atomEnv = global.buildAtomEnvironment();
+ repository = await buildRepository(await cloneRepository('multiple-commits'));
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ });
+
+ function buildApp(override = {}) {
+ const props = {
+ repository,
+ commit: commitBuilder().build(),
+ messageCollapsible: false,
+ messageOpen: true,
+ itemType: CommitDetailItem,
+
+ workspace: atomEnv.workspace,
+ commands: atomEnv.commands,
+ keymaps: atomEnv.keymaps,
+ tooltips: atomEnv.tooltips,
+ config: atomEnv.config,
+
+ destroy: () => {},
+ toggleMessage: () => {},
+ ...override,
+ };
+
+ return ;
+ }
+
+ it('has a MultiFilePatchController that its itemType set', function() {
+ const wrapper = shallow(buildApp({itemType: CommitDetailItem}));
+ assert.strictEqual(wrapper.find('MultiFilePatchController').prop('itemType'), CommitDetailItem);
+ });
+
+ 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 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',
+ );
+ });
+
+ 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',
+ ],
+ );
+ });
+
+ describe('commit message collapsibility', function() {
+ 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.';
+ });
+
+ 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.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), shortMessage);
+ });
+
+ 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('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 a button to reveal the rest of the message', function() {
+ const button = wrapper.find('.github-CommitDetailView-moreButton');
+ assert.lengthOf(button, 1);
+ 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.strictEqual(wrapper.find('.github-CommitDetailView-moreText').text(), longMessage);
+ });
+
+ it('renders a button to collapse the message text', function() {
+ const button = wrapper.find('.github-CommitDetailView-moreButton');
+ assert.lengthOf(button, 1);
+ assert.strictEqual(button.text(), 'Show Less');
+ });
+
+ it('the button calls toggleMessage when clicked', function() {
+ const button = wrapper.find('.github-CommitDetailView-moreButton');
+ button.simulate('click');
+ assert.isTrue(toggleMessage.called);
+ });
+ });
+ });
+});
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/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);
+ });
});
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());
+ })
});