Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit b1f5c03

Browse files
authored
Merge pull request #1822 from atom/vy/merge-conflict-pane
Fix: git tab keeps popping open when there's a merge conflict
2 parents 83fe60b + d9875d4 commit b1f5c03

File tree

6 files changed

+253
-17
lines changed

6 files changed

+253
-17
lines changed

lib/controllers/root-controller.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export default class RootController extends React.Component {
9393
});
9494

9595
this.subscription = new CompositeDisposable(
96-
this.props.repository.onMergeError(this.gitTabTracker.ensureVisible),
96+
this.props.repository.onPullError(this.gitTabTracker.ensureVisible),
9797
);
9898

9999
this.props.commandRegistry.onDidDispatch(event => {
@@ -175,7 +175,6 @@ export default class RootController extends React.Component {
175175
confirm={this.props.confirm}
176176
toggleGitTab={this.gitTabTracker.toggle}
177177
toggleGithubTab={this.githubTabTracker.toggle}
178-
ensureGitTabVisible={this.gitTabTracker.ensureVisible}
179178
/>
180179
</StatusBar>
181180
);
@@ -453,7 +452,7 @@ export default class RootController extends React.Component {
453452
componentDidUpdate() {
454453
this.subscription.dispose();
455454
this.subscription = new CompositeDisposable(
456-
this.props.repository.onMergeError(() => this.gitTabTracker.ensureVisible()),
455+
this.props.repository.onPullError(() => this.gitTabTracker.ensureVisible()),
457456
);
458457
}
459458

lib/controllers/status-bar-tile-controller.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export default class StatusBarTileController extends React.Component {
2222
repository: PropTypes.object.isRequired,
2323
toggleGitTab: PropTypes.func,
2424
toggleGithubTab: PropTypes.func,
25-
ensureGitTabVisible: PropTypes.func,
2625
}
2726

2827
constructor(props) {
@@ -75,9 +74,6 @@ export default class StatusBarTileController extends React.Component {
7574
mergeConflictsPresent = Object.keys(data.statusesForChangedFiles.mergeConflictFiles).length > 0;
7675
}
7776

78-
if (mergeConflictsPresent) {
79-
this.props.ensureGitTabVisible();
80-
}
8177
const repoProps = {
8278
repository: this.props.repository,
8379
currentBranch: data.currentBranch,

lib/get-repo-pipeline-manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export default function({confirm, notificationManager, workspace}) {
7272
return result;
7373
} catch (error) {
7474
if (error instanceof GitError) {
75+
repository.didPullError();
7576
if (/error: Your local changes to the following files would be overwritten by merge/.test(error.stdErr)) {
7677
const lines = error.stdErr.split('\n');
7778
const files = lines.slice(3, lines.length - 3).map(l => `\`${l.trim()}\``).join('\n');
@@ -82,7 +83,6 @@ export default function({confirm, notificationManager, workspace}) {
8283
dismissable: true,
8384
});
8485
} else if (/Automatic merge failed; fix conflicts and then commit the result./.test(error.stdOut)) {
85-
repository.didMergeError();
8686
notificationManager.addWarning('Merge conflicts', {
8787
description: `Your local changes conflicted with changes made on the remote branch. Resolve the conflicts
8888
with the Git panel and commit to continue.`,

lib/models/repository.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ export default class Repository {
108108
return this.emitter.on('did-update', callback);
109109
}
110110

111-
onMergeError(callback) {
112-
return this.emitter.on('merge-error', callback);
111+
onPullError(callback) {
112+
return this.emitter.on('pull-error', callback);
113113
}
114114

115-
didMergeError() {
116-
return this.emitter.emit('merge-error');
115+
didPullError() {
116+
return this.emitter.emit('pull-error');
117117
}
118118

119119
// State-independent actions /////////////////////////////////////////////////////////////////////////////////////////

test/controllers/status-bar-tile-controller.test.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ describe('StatusBarTileController', function() {
4242
confirm={confirm}
4343
toggleGitTab={() => {}}
4444
toggleGithubTab={() => {}}
45-
ensureGitTabVisible={() => {}}
4645
{...props}
4746
/>
4847
);
@@ -622,14 +621,13 @@ describe('StatusBarTileController', function() {
622621
await assert.async.isFalse(repository.getOperationStates().isPushInProgress());
623622
});
624623

625-
626624
it('displays a warning notification when pull results in merge conflicts', async function() {
627625
const {localRepoPath} = await setUpLocalAndRemoteRepositories('multiple-commits', {remoteAhead: true});
628626
fs.writeFileSync(path.join(localRepoPath, 'file.txt'), 'apple');
629627
const repository = await buildRepositoryWithPipeline(localRepoPath, {confirm, notificationManager, workspace});
630628
await repository.git.exec(['commit', '-am', 'Add conflicting change']);
631629

632-
const wrapper = await mountAndLoad(buildApp({repository, ensureGitTabVisible: sinon.stub()}));
630+
const wrapper = await mountAndLoad(buildApp({repository}));
633631

634632
sinon.stub(notificationManager, 'addWarning');
635633

@@ -640,8 +638,6 @@ describe('StatusBarTileController', function() {
640638
}
641639
repository.refresh();
642640

643-
await assert.async.isTrue(wrapper.instance().props.ensureGitTabVisible.called);
644-
645641
await assert.async.isTrue(notificationManager.addWarning.called);
646642
const notificationArgs = notificationManager.addWarning.args[0];
647643
assert.equal(notificationArgs[0], 'Merge conflicts');
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import {cloneRepository, buildRepositoryWithPipeline} from './helpers';
2+
import {GitError} from '../lib/git-shell-out-strategy';
3+
4+
5+
describe('getRepoPipelineManager()', function() {
6+
7+
let atomEnv, workspace, notificationManager, repo, pipelineManager, confirm;
8+
9+
const getPipeline = (pm, actionName) => {
10+
const actionKey = pm.actionKeys[actionName];
11+
return pm.getPipeline(actionKey);
12+
};
13+
14+
const buildRepo = (workdir, override = {}) => {
15+
const option = {
16+
confirm,
17+
notificationManager,
18+
workspace,
19+
...override,
20+
};
21+
return buildRepositoryWithPipeline(workdir, option);
22+
};
23+
24+
const gitErrorStub = (stdErr = '', stdOut = '') => {
25+
return sinon.stub().throws(() => {
26+
const err = new GitError();
27+
err.stdErr = stdErr;
28+
err.stdOut = stdOut;
29+
return err;
30+
});
31+
};
32+
33+
beforeEach(async function() {
34+
atomEnv = global.buildAtomEnvironment();
35+
workspace = atomEnv.workspace;
36+
notificationManager = atomEnv.notifications;
37+
confirm = sinon.stub(atomEnv, 'confirm');
38+
39+
const workdir = await cloneRepository('multiple-commits');
40+
repo = await buildRepo(workdir);
41+
pipelineManager = repo.pipelineManager;
42+
});
43+
44+
afterEach(function() {
45+
atomEnv.destroy();
46+
});
47+
48+
it('has all the action pipelines', function() {
49+
const expectedActions = ['PUSH', 'PULL', 'FETCH', 'COMMIT', 'CHECKOUT', 'ADDREMOTE'];
50+
for (const actionName of expectedActions) {
51+
assert.ok(getPipeline(pipelineManager, actionName));
52+
}
53+
});
54+
55+
describe('PUSH pipeline', function() {
56+
57+
it('confirm-force-push', function() {
58+
it('before confirming', function() {
59+
const pushPipeline = getPipeline(pipelineManager, 'PUSH');
60+
const pushStub = sinon.stub();
61+
sinon.spy(notificationManager, 'addError');
62+
pushPipeline.run(pushStub, repo, '', {force: true});
63+
assert.isTrue(confirm.calledWith({
64+
message: 'Are you sure you want to force push?',
65+
detailedMessage: 'This operation could result in losing data on the remote.',
66+
buttons: ['Force Push', 'Cancel'],
67+
}));
68+
assert.isTrue(pushStub.called());
69+
});
70+
71+
it('after confirming', async function() {
72+
const nWorkdir = await cloneRepository('multiple-commits');
73+
const confirmStub = sinon.stub(atomEnv, 'confirm').return(0);
74+
const nRepo = buildRepo(nWorkdir, {confirm: confirmStub});
75+
const pushPipeline = getPipeline(nRepo.pipelineManager, 'PUSH');
76+
const pushStub = sinon.stub();
77+
sinon.spy(notificationManager, 'addError');
78+
79+
pushPipeline.run(pushStub, repo, '', {force: true});
80+
assert.isFalse(confirm.called);
81+
assert.isFalse(pushStub.called);
82+
});
83+
});
84+
85+
it('set-push-in-progress', async function() {
86+
const pushPipeline = getPipeline(pipelineManager, 'PUSH');
87+
const pushStub = sinon.stub().callsFake(() => {
88+
assert.isTrue(repo.getOperationStates().isPushInProgress());
89+
return Promise.resolve();
90+
});
91+
pushPipeline.run(pushStub, repo, '', {});
92+
assert.isTrue(pushStub.called);
93+
await assert.async.isFalse(repo.getOperationStates().isPushInProgress());
94+
});
95+
96+
it('failed-to-push-error', function() {
97+
const pushPipeline = getPipeline(pipelineManager, 'PUSH');
98+
sinon.spy(notificationManager, 'addError');
99+
100+
101+
pushPipeline.run(gitErrorStub('rejected failed to push'), repo, '', {});
102+
assert.isTrue(notificationManager.addError.calledWithMatch('Push rejected', {dismissable: true}));
103+
104+
pushPipeline.run(gitErrorStub('something else'), repo, '', {});
105+
assert.isTrue(notificationManager.addError.calledWithMatch('Unable to push', {dismissable: true}));
106+
});
107+
});
108+
109+
describe('PULL pipeline', function() {
110+
it('set-pull-in-progress', async function() {
111+
const pull = getPipeline(pipelineManager, 'PULL');
112+
const pullStub = sinon.stub().callsFake(() => {
113+
assert.isTrue(repo.getOperationStates().isPullInProgress());
114+
return Promise.resolve();
115+
});
116+
pull.run(pullStub, repo, '', {});
117+
assert.isTrue(pullStub.called);
118+
await assert.async.isFalse(repo.getOperationStates().isPullInProgress());
119+
});
120+
121+
it('failed-to-pull-error', function() {
122+
const pullPipeline = getPipeline(pipelineManager, 'PULL');
123+
sinon.spy(notificationManager, 'addError');
124+
sinon.spy(notificationManager, 'addWarning');
125+
126+
pullPipeline.run(gitErrorStub('error: Your local changes to the following files would be overwritten by merge:\n\ta.txt\n\tb.txt'), repo, '', {});
127+
assert.isTrue(notificationManager.addError.calledWithMatch('Pull aborted', {dismissable: true}));
128+
129+
pullPipeline.run(gitErrorStub('', 'Automatic merge failed; fix conflicts and then commit the result.'), repo, '', {});
130+
assert.isTrue(notificationManager.addWarning.calledWithMatch('Merge conflicts', {dismissable: true}));
131+
132+
pullPipeline.run(gitErrorStub('fatal: Not possible to fast-forward, aborting.'), repo, '', {});
133+
assert.isTrue(notificationManager.addWarning.calledWithMatch('Unmerged changes', {dismissable: true}));
134+
135+
pullPipeline.run(gitErrorStub('something else'), repo, '', {});
136+
assert.isTrue(notificationManager.addError.calledWithMatch('Unable to pull', {dismissable: true}));
137+
});
138+
});
139+
140+
describe('FETCH pipeline', function() {
141+
let fetchPipeline;
142+
143+
beforeEach(function() {
144+
fetchPipeline = getPipeline(pipelineManager, 'FETCH');
145+
});
146+
147+
it('set-fetch-in-progress', async function() {
148+
const fetchStub = sinon.stub().callsFake(() => {
149+
assert.isTrue(repo.getOperationStates().isFetchInProgress());
150+
return Promise.resolve();
151+
});
152+
fetchPipeline.run(fetchStub, repo, '', {});
153+
assert.isTrue(fetchStub.called);
154+
await assert.async.isFalse(repo.getOperationStates().isFetchInProgress());
155+
});
156+
157+
it('failed-to-fetch-error', function() {
158+
sinon.spy(notificationManager, 'addError');
159+
160+
fetchPipeline.run(gitErrorStub('this is a nice error msg'), repo, '', {});
161+
assert.isTrue(notificationManager.addError.calledWithMatch('Unable to fetch', {
162+
detail: 'this is a nice error msg',
163+
dismissable: true,
164+
}));
165+
});
166+
});
167+
168+
describe('CHECKOUT pipeline', function() {
169+
let checkoutPipeline;
170+
171+
beforeEach(function() {
172+
checkoutPipeline = getPipeline(pipelineManager, 'CHECKOUT');
173+
});
174+
175+
it('set-checkout-in-progress', async function() {
176+
const checkoutStub = sinon.stub().callsFake(() => {
177+
assert.isTrue(repo.getOperationStates().isCheckoutInProgress());
178+
return Promise.resolve();
179+
});
180+
checkoutPipeline.run(checkoutStub, repo, '', {});
181+
assert.isTrue(checkoutStub.called);
182+
await assert.async.isFalse(repo.getOperationStates().isCheckoutInProgress());
183+
});
184+
185+
it('failed-to-checkout-error', function() {
186+
sinon.spy(notificationManager, 'addError');
187+
188+
checkoutPipeline.run(gitErrorStub('local changes would be overwritten: \n\ta.txt\n\tb.txt'), repo, '', {createNew: false});
189+
assert.isTrue(notificationManager.addError.calledWithMatch('Checkout aborted', {dismissable: true}));
190+
191+
checkoutPipeline.run(gitErrorStub('branch x already exists'), repo, '', {createNew: false});
192+
assert.isTrue(notificationManager.addError.calledWithMatch('Checkout aborted', {dismissable: true}));
193+
194+
checkoutPipeline.run(gitErrorStub('error: you need to resolve your current index first'), repo, '', {createNew: false});
195+
assert.isTrue(notificationManager.addError.calledWithMatch('Checkout aborted', {dismissable: true}));
196+
197+
checkoutPipeline.run(gitErrorStub('something else'), repo, '', {createNew: true});
198+
assert.isTrue(notificationManager.addError.calledWithMatch('Cannot create branch', {detail: 'something else', dismissable: true}));
199+
});
200+
});
201+
202+
describe('COMMIT pipeline', function() {
203+
let commitPipeline;
204+
205+
beforeEach(function() {
206+
commitPipeline = getPipeline(pipelineManager, 'COMMIT');
207+
});
208+
209+
it('set-commit-in-progress', async function() {
210+
const commitStub = sinon.stub().callsFake(() => {
211+
assert.isTrue(repo.getOperationStates().isCommitInProgress());
212+
return Promise.resolve();
213+
});
214+
commitPipeline.run(commitStub, repo, '', {});
215+
assert.isTrue(commitStub.called);
216+
await assert.async.isFalse(repo.getOperationStates().isCommitInProgress());
217+
});
218+
219+
it('failed-to-commit-error', function() {
220+
sinon.spy(notificationManager, 'addError');
221+
222+
commitPipeline.run(gitErrorStub('a nice msg'), repo, '', {});
223+
assert.isTrue(notificationManager.addError.calledWithMatch('Unable to commit', {detail: 'a nice msg', dismissable: true}));
224+
});
225+
});
226+
227+
describe('ADDREMOTE pipeline', function() {
228+
it('failed-to-add-remote', function() {
229+
const addRemotePipeline = getPipeline(pipelineManager, 'ADDREMOTE');
230+
sinon.spy(notificationManager, 'addError');
231+
232+
addRemotePipeline.run(gitErrorStub('fatal: remote x already exists.'), repo, 'existential-crisis');
233+
assert.isTrue(notificationManager.addError.calledWithMatch('Cannot create remote', {
234+
detail: 'The repository already contains a remote named existential-crisis.',
235+
dismissable: true,
236+
}));
237+
238+
addRemotePipeline.run(gitErrorStub('something else'), repo, 'remotename');
239+
assert.isTrue(notificationManager.addError.calledWithMatch('Cannot create remote', {
240+
detail: 'something else',
241+
dismissable: true,
242+
}));
243+
});
244+
});
245+
});

0 commit comments

Comments
 (0)