+
Fetch
-
+
Pull {this.props.behindCount ? `(${this.props.behindCount})` : ''}
-
+
Push {this.props.aheadCount ? `(${this.props.aheadCount})` : ''}
@@ -73,8 +73,12 @@ export default class PushPullMenuView extends React.Component {
return this.state.errorMessage;
}
+ if (this.props.currentBranch.isDetached) {
+ return 'Note: you are not on a branch. Please create one if you wish to push your work anywhere.';
+ }
+
if (!this.props.remoteName) {
- return `Note: No remote detected for branch ${this.props.branchName}. ` +
+ return `Note: No remote detected for branch ${this.props.currentBranch.name}. ` +
'Pushing will set up a remote tracking branch on remote repo "origin"';
}
diff --git a/styles/status-bar-tile-controller.less b/styles/status-bar-tile-controller.less
index b00b34f352..21bdfa78fe 100644
--- a/styles/status-bar-tile-controller.less
+++ b/styles/status-bar-tile-controller.less
@@ -41,4 +41,8 @@
animation: github-StatusBarSync-animation 2s linear 30; // limit to 1min in case something gets stuck
}
+ .github-branch-detached {
+ color: @text-color-subtle;
+ font-style: italic;
+ }
}
diff --git a/test/controllers/commit-view-controller.test.js b/test/controllers/commit-view-controller.test.js
index bf6a66d18e..e29206c8bd 100644
--- a/test/controllers/commit-view-controller.test.js
+++ b/test/controllers/commit-view-controller.test.js
@@ -2,11 +2,13 @@ import CommitViewController from '../../lib/controllers/commit-view-controller';
import {cloneRepository, buildRepository} from '../helpers';
describe('CommitViewController', function() {
- let atomEnvironment, commandRegistry;
+ let atomEnvironment, commandRegistry, lastCommit;
beforeEach(function() {
atomEnvironment = global.buildAtomEnvironment();
commandRegistry = atomEnvironment.commands;
+
+ lastCommit = {sha: 'a1e23fd45', message: 'last commit message', unbornRef: false};
});
afterEach(function() {
@@ -18,7 +20,7 @@ describe('CommitViewController', function() {
const repository1 = await buildRepository(workdirPath1);
const workdirPath2 = await cloneRepository('three-files');
const repository2 = await buildRepository(workdirPath2);
- const controller = new CommitViewController({commandRegistry, repository: repository1});
+ const controller = new CommitViewController({commandRegistry, lastCommit, repository: repository1});
assert.equal(controller.regularCommitMessage, '');
assert.equal(controller.amendingCommitMessage, '');
@@ -36,13 +38,12 @@ describe('CommitViewController', function() {
});
describe('the passed commit message', function() {
- let controller, commitView, lastCommit;
+ let controller, commitView;
beforeEach(async function() {
const workdirPath = await cloneRepository('three-files');
const repository = await buildRepository(workdirPath);
- controller = new CommitViewController({commandRegistry, repository});
+ controller = new CommitViewController({commandRegistry, lastCommit, repository});
commitView = controller.refs.commitView;
- lastCommit = {sha: 'a1e23fd45', message: 'last commit message'};
});
it('is set to the regularCommitMessage in the default case', async function() {
diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js
index b594211e7a..d7b18ffc72 100644
--- a/test/controllers/root-controller.test.js
+++ b/test/controllers/root-controller.test.js
@@ -9,13 +9,15 @@ import {cloneRepository, buildRepository} from '../helpers';
import RootController from '../../lib/controllers/root-controller';
describe('RootController', function() {
- let atomEnv, workspace, commandRegistry, notificationManager, confirm, app;
+ let atomEnv, workspace, commandRegistry, notificationManager, tooltips, confirm, app;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
workspace = atomEnv.workspace;
commandRegistry = atomEnv.commands;
notificationManager = atomEnv.notifications;
+ tooltips = atomEnv.tooltips;
+
confirm = sinon.stub(atomEnv, 'confirm');
app = (
@@ -23,6 +25,7 @@ describe('RootController', function() {
workspace={workspace}
commandRegistry={commandRegistry}
notificationManager={notificationManager}
+ tooltips={tooltips}
confirm={confirm}
/>
);
diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js
index fb42481389..0ca38447f2 100644
--- a/test/controllers/status-bar-tile-controller.test.js
+++ b/test/controllers/status-bar-tile-controller.test.js
@@ -54,7 +54,20 @@ describe('StatusBarTileController', function() {
const wrapper = mount(React.cloneElement(component, {repository}));
await wrapper.instance().refreshModelData();
- assert.equal(wrapper.find(BranchView).prop('branchName'), 'master');
+ assert.equal(wrapper.find(BranchView).prop('currentBranch').name, 'master');
+ assert.lengthOf(wrapper.find(BranchView).find('.github-branch-detached'), 0);
+ });
+
+ it('styles a detached HEAD differently', async function() {
+ const workdirPath = await cloneRepository('multiple-commits');
+ const repository = await buildRepository(workdirPath);
+ await repository.checkout('HEAD~2');
+
+ const wrapper = mount(React.cloneElement(component, {repository}));
+ await wrapper.instance().refreshModelData();
+
+ assert.equal(wrapper.find(BranchView).prop('currentBranch').name, 'master~2');
+ assert.lengthOf(wrapper.find(BranchView).find('.github-branch-detached'), 1);
});
describe('the branch menu', function() {
@@ -84,17 +97,17 @@ describe('StatusBarTileController', function() {
const branches = Array.from(tip.getElementsByTagName('option'), e => e.innerHTML);
assert.deepEqual(branches, ['branch', 'master']);
- assert.equal(await repository.getCurrentBranch(), 'master');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'master', isDetached: false});
assert.equal(tip.querySelector('select').value, 'master');
selectOption(tip, 'branch');
- assert.equal(await repository.getCurrentBranch(), 'branch');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
assert.equal(tip.querySelector('select').value, 'branch');
await wrapper.instance().refreshModelData();
assert.equal(tip.querySelector('select').value, 'branch');
selectOption(tip, 'master');
- assert.equal(await repository.getCurrentBranch(), 'master');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'master', isDetached: false});
assert.equal(tip.querySelector('select').value, 'master');
await wrapper.instance().refreshModelData();
assert.equal(tip.querySelector('select').value, 'master');
@@ -117,7 +130,7 @@ describe('StatusBarTileController', function() {
const tip = getTooltipNode(wrapper, BranchView);
- assert.equal(await repository.getCurrentBranch(), 'branch');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
assert.equal(tip.querySelector('select').value, 'branch');
sinon.stub(notificationManager, 'addError');
@@ -149,7 +162,7 @@ describe('StatusBarTileController', function() {
const branches = Array.from(tip.querySelectorAll('option'), option => option.value);
assert.deepEqual(branches, ['master']);
- assert.equal(await repository.getCurrentBranch(), 'master');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'master', isDetached: false});
assert.equal(tip.querySelector('select').value, 'master');
tip.querySelector('button').click();
@@ -166,7 +179,7 @@ describe('StatusBarTileController', function() {
});
assert.equal(tip.querySelector('select').value, 'new-branch');
- assert.equal(await repository.getCurrentBranch(), 'new-branch');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'new-branch', isDetached: false});
assert.lengthOf(tip.querySelectorAll('.github-BranchMenuView-editor'), 0);
assert.lengthOf(tip.querySelectorAll('select'), 1);
@@ -185,7 +198,7 @@ describe('StatusBarTileController', function() {
const branches = Array.from(tip.getElementsByTagName('option'), option => option.value);
assert.deepEqual(branches, ['branch', 'master']);
- assert.equal(await repository.getCurrentBranch(), 'branch');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
assert.equal(tip.querySelector('select').value, 'branch');
tip.querySelector('button').click();
@@ -197,10 +210,27 @@ describe('StatusBarTileController', function() {
assert.equal(notificationArgs[0], 'Cannot create branch');
assert.match(notificationArgs[1].description, /already exists/);
- assert.equal(await repository.getCurrentBranch(), 'branch');
+ assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
assert.equal(tip.querySelector('select').value, 'branch');
});
});
+
+ describe('with a detached HEAD', function() {
+ it('includes the current describe output as a disabled option', async function() {
+ const workdirPath = await cloneRepository('multiple-commits');
+ const repository = await buildRepository(workdirPath);
+ await repository.checkout('HEAD~2');
+
+ const wrapper = mount(React.cloneElement(component, {repository}));
+ await wrapper.instance().refreshModelData();
+
+ const tip = getTooltipNode(wrapper, BranchView);
+ assert.equal(tip.querySelector('select').value, 'detached');
+ const option = tip.querySelector('option[value="detached"]');
+ assert.equal(option.textContent, 'master~2');
+ assert.isTrue(option.disabled);
+ });
+ });
});
});
@@ -314,6 +344,33 @@ describe('StatusBarTileController', function() {
await assert.async.equal(pushButton.textContent, 'Push ');
await assert.async.equal(pullButton.textContent, 'Pull ');
});
+
+ describe('with a detached HEAD', function() {
+ let wrapper;
+
+ beforeEach(async function() {
+ const workdirPath = await cloneRepository('multiple-commits');
+ const repository = await buildRepository(workdirPath);
+ await repository.checkout('HEAD~2');
+
+ wrapper = mount(React.cloneElement(component, {repository}));
+ await wrapper.instance().refreshModelData();
+ });
+
+ it('disables the fetch, pull, and push buttons', function() {
+ const tip = getTooltipNode(wrapper, PushPullView);
+
+ assert.isTrue(tip.querySelector('button.github-PushPullMenuView-pull').disabled);
+ assert.isTrue(tip.querySelector('button.github-PushPullMenuView-push').disabled);
+ });
+
+ it('displays an appropriate explanation', function() {
+ const tip = getTooltipNode(wrapper, PushPullView);
+
+ const message = tip.querySelector('.github-PushPullMenuView-message');
+ assert.match(message.textContent, /not on a branch/);
+ });
+ });
});
describe('fetch and pull commands', function() {
diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js
index eabd02d9b6..278eb2f09e 100644
--- a/test/git-strategies.test.js
+++ b/test/git-strategies.test.js
@@ -6,7 +6,7 @@ import mkdirp from 'mkdirp';
import CompositeGitStrategy from '../lib/composite-git-strategy';
import GitShellOutStrategy, {GitError} from '../lib/git-shell-out-strategy';
-import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories} from './helpers';
+import {cloneRepository, initRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories} from './helpers';
/**
* KU Thoughts: The GitShellOutStrategy methods are tested in Repository tests for the most part
@@ -136,6 +136,26 @@ import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories
});
});
+ describe('getHeadCommit()', function() {
+ it('gets the SHA and message of the most recent commit', async function() {
+ const workingDirPath = await cloneRepository('three-files');
+ const git = createTestStrategy(workingDirPath);
+
+ const commit = await git.getHeadCommit();
+ assert.equal(commit.sha, '66d11860af6d28eb38349ef83de475597cb0e8b4');
+ assert.equal(commit.message, 'Initial commit');
+ assert.isFalse(commit.unbornRef);
+ });
+
+ it('notes when HEAD is an unborn ref', async function() {
+ const workingDirPath = await initRepository();
+ const git = createTestStrategy(workingDirPath);
+
+ const commit = await git.getHeadCommit();
+ assert.isTrue(commit.unbornRef);
+ });
+ });
+
describe('diffFileStatus', function() {
it('returns an object with working directory file diff status between relative to specified target commit', async function() {
const workingDirPath = await cloneRepository('three-files');
@@ -492,15 +512,30 @@ import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories
it('returns the current branch name', async function() {
const workingDirPath = await cloneRepository('merge-conflict');
const git = createTestStrategy(workingDirPath);
- assert.equal(await git.getCurrentBranch(), 'master');
+ assert.deepEqual(await git.getCurrentBranch(), {name: 'master', isDetached: false});
await git.checkout('branch');
- assert.equal(await git.getCurrentBranch(), 'branch');
+ assert.deepEqual(await git.getCurrentBranch(), {name: 'branch', isDetached: false});
// newBranch does not yet exist
await assert.isRejected(git.checkout('newBranch'));
- assert.equal(await git.getCurrentBranch(), 'branch');
+ assert.deepEqual(await git.getCurrentBranch(), {name: 'branch', isDetached: false});
await git.checkout('newBranch', {createNew: true});
- assert.equal(await git.getCurrentBranch(), 'newBranch');
+ assert.deepEqual(await git.getCurrentBranch(), {name: 'newBranch', isDetached: false});
+ });
+
+ it('returns the current branch name in a repository with no commits', async function() {
+ const workingDirPath = await initRepository();
+ const git = createTestStrategy(workingDirPath);
+
+ assert.deepEqual(await git.getCurrentBranch(), {name: 'master', isDetached: false});
+ });
+
+ it('returns a reasonable default in a repository with a detached HEAD', async function() {
+ const workingDirPath = await cloneRepository('multiple-commits');
+ const git = createTestStrategy(workingDirPath);
+ await git.exec(['checkout', 'HEAD^']);
+
+ assert.deepEqual(await git.getCurrentBranch(), {name: 'master~1', isDetached: true});
});
});
diff --git a/test/helpers.js b/test/helpers.js
index 2cd868ca40..73b873fc6b 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -37,6 +37,16 @@ export async function cloneRepository(repoName = 'three-files') {
return copyCachedRepo(repoName);
}
+/*
+ * Initialize an empty repository at a temporary path.
+ */
+export async function initRepository(repoName) {
+ const workingDirPath = temp.mkdirSync('git-fixture-');
+ const git = new GitShellOutStrategy(workingDirPath);
+ await git.exec(['init']);
+ return fs.realpathSync(workingDirPath);
+}
+
export async function setUpLocalAndRemoteRepositories(repoName = 'multiple-commits', options = {}) {
/* eslint-disable no-param-reassign */
if (typeof repoName === 'object') {
diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js
index aa638ceb70..5c42e20417 100644
--- a/test/views/commit-view.test.js
+++ b/test/views/commit-view.test.js
@@ -5,11 +5,13 @@ import until from 'test-until';
import CommitView from '../../lib/views/commit-view';
describe('CommitView', function() {
- let atomEnv, commandRegistry;
+ let atomEnv, commandRegistry, lastCommit;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
commandRegistry = atomEnv.commands;
+
+ lastCommit = {sha: '1234abcd', message: 'commit message', unbornRef: false};
});
afterEach(function() {
@@ -17,7 +19,10 @@ describe('CommitView', function() {
});
it('displays the remaining characters limit based on which line is being edited', async function() {
- const view = new CommitView({commandRegistry, stagedChangesExist: true, maximumCharacterLimit: 72, message: ''});
+ const view = new CommitView({
+ commandRegistry, lastCommit,
+ stagedChangesExist: true, maximumCharacterLimit: 72, message: '',
+ });
assert.equal(view.refs.remainingCharacters.textContent, '72');
await view.update({message: 'abcde fghij'});
@@ -64,7 +69,10 @@ describe('CommitView', function() {
const workdirPath = await cloneRepository('three-files');
const repository = await buildRepository(workdirPath);
const viewState = {};
- view = new CommitView({repository, commandRegistry, stagedChangesExist: true, mergeConflictsExist: false, viewState});
+ view = new CommitView({
+ repository, commandRegistry, lastCommit,
+ stagedChangesExist: true, mergeConflictsExist: false, viewState,
+ });
editor = view.refs.editor;
commitButton = view.refs.commitButton;
@@ -107,7 +115,10 @@ describe('CommitView', function() {
const prepareToCommit = () => Promise.resolve(prepareToCommitResolution);
commit = sinon.spy();
- view = new CommitView({commandRegistry, stagedChangesExist: true, prepareToCommit, commit, message: 'Something'});
+ view = new CommitView({
+ commandRegistry, lastCommit,
+ stagedChangesExist: true, prepareToCommit, commit, message: 'Something',
+ });
sinon.spy(view, 'focus');
editor = view.refs.editor;
@@ -158,20 +169,22 @@ describe('CommitView', function() {
});
it('shows the "Abort Merge" button when props.isMerging is true', async function() {
- const view = new CommitView({commandRegistry, stagedChangesExist: true, isMerging: false});
- const {abortMergeButton} = view.refs;
- assert.equal(abortMergeButton.style.display, 'none');
+ const view = new CommitView({commandRegistry, lastCommit, stagedChangesExist: true, isMerging: false});
+ assert.isUndefined(view.refs.abortMergeButton);
await view.update({isMerging: true});
- assert.equal(abortMergeButton.style.display, '');
+ assert.isDefined(view.refs.abortMergeButton);
await view.update({isMerging: false});
- assert.equal(abortMergeButton.style.display, 'none');
+ assert.isUndefined(view.refs.abortMergeButton);
});
it('calls props.abortMerge() when the "Abort Merge" button is clicked', function() {
const abortMerge = sinon.spy(() => Promise.resolve());
- const view = new CommitView({commandRegistry, stagedChangesExist: true, isMerging: true, abortMerge});
+ const view = new CommitView({
+ commandRegistry, lastCommit,
+ stagedChangesExist: true, isMerging: true, abortMerge,
+ });
const {abortMergeButton} = view.refs;
abortMergeButton.dispatchEvent(new MouseEvent('click'));
assert(abortMerge.calledOnce);
@@ -180,7 +193,10 @@ describe('CommitView', function() {
describe('amending', function() {
it('calls props.setAmending() when the box is checked or unchecked', function() {
const setAmending = sinon.spy();
- const view = new CommitView({commandRegistry, stagedChangesExist: false, lastCommit: {message: 'previous commit\'s message'}, setAmending});
+ const view = new CommitView({
+ commandRegistry,
+ stagedChangesExist: false, lastCommit: {message: 'previous commit\'s message'}, setAmending,
+ });
const {amend} = view.refs;
amend.click();
@@ -189,5 +205,14 @@ describe('CommitView', function() {
amend.click();
assert.deepEqual(setAmending.args, [[true], [false]]);
});
+
+ it('is hidden when HEAD is an unborn ref', function() {
+ const view = new CommitView({
+ commandRegistry,
+ stagedChangesExist: false,
+ lastCommit: {unbornRef: true},
+ });
+ assert.isUndefined(view.refs.amend);
+ });
});
});