From e9b883cec557e2d94666b77e3d1b9733ac69869c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 30 Mar 2017 15:47:42 -0400
Subject: [PATCH 001/161] (Hopefully) cleaner model and Promise management in
GithubPackage
---
lib/github-package.js | 99 +++++++++++++++++--------------------------
1 file changed, 38 insertions(+), 61 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 83f9390a9d..0e8fa8d835 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -34,6 +34,8 @@ export default class GithubPackage {
this.modelPromisesByProjectPath = new Map();
this.modelsByProjectPath = new Map();
this.confirm = confirm;
+
+ this.activeProjectPath = null;
this.activeRepository = null;
this.activeResolutionProgress = null;
@@ -46,9 +48,9 @@ export default class GithubPackage {
this.subscriptions = new CompositeDisposable();
const projectPaths = this.project.getDirectories().map(dir => dir.getRealPathSync());
- const initPromises = this.cacheModelsForPaths(projectPaths);
- this.initWatchersStartedPromise = initPromises.changeObserversStartedPromise;
- this.initModelsPromise = initPromises.modelsPromise;
+ if (projectPaths.length > 0) {
+ this.didChangeProjectPaths(projectPaths);
+ }
this.subscriptions.add(
this.project.onDidChangePaths(this.didChangeProjectPaths),
@@ -102,23 +104,8 @@ export default class GithubPackage {
}
async deserialize(state) {
- // Ensure that Repository promises are available
- await this.initModelsPromise;
- let nextActiveRepository, nextActiveResolutionProgress;
-
if (state.activeRepositoryPath) {
- const repository = await this.getRepositoryForWorkdirPath(state.activeRepositoryPath);
- if (repository) { nextActiveRepository = repository; }
- }
-
- if (nextActiveRepository) {
- const savedResolutionProgress = this.savedState.resolutionProgressByPath[state.activeRepositoryPath] || {};
- const commit = await nextActiveRepository.getLastCommit();
- nextActiveResolutionProgress = new ResolutionProgress(commit.sha, savedResolutionProgress);
-
- this.setActiveModels(nextActiveRepository, nextActiveResolutionProgress);
- } else {
- await this.updateActiveModels();
+ await this.didChangeProjectPaths([state.activeRepositoryPath]);
}
}
@@ -170,11 +157,13 @@ export default class GithubPackage {
@autobind
async didChangeProjectPaths(projectPaths) {
- this.updateActiveModels();
const previousProjectPaths = Array.from(this.modelPromisesByProjectPath.keys());
const {added, removed} = compareSets(new Set(previousProjectPaths), new Set(projectPaths));
- this.cacheModelsForPaths(Array.from(added));
- await this.destroyModelsForPaths(Array.from(removed));
+ await Promise.all([
+ this.cacheModelsForPaths(Array.from(added)),
+ this.destroyModelsForPaths(Array.from(removed)),
+ ]);
+ await this.updateActiveModels();
}
@autobind
@@ -200,14 +189,6 @@ export default class GithubPackage {
this.updateActiveModels();
}
- getInitialWatchersStartedPromise() {
- return this.initWatchersStartedPromise;
- }
-
- getInitialModelsPromise() {
- return this.initModelsPromise;
- }
-
getActiveProjectPath() {
return this.activeProjectPath;
}
@@ -221,8 +202,9 @@ export default class GithubPackage {
}
async updateActiveModels() {
- await this.initModelsPromise;
- let nextActiveRepository, nextActiveResolutionProgress, nextActiveProjectPath;
+ let nextActiveRepository = null;
+ let nextActiveResolutionProgress = null;
+ let nextActiveProjectPath = null;
const activeItem = this.workspace.getActivePaneItem();
if (activeItem && typeof activeItem.getPath === 'function') {
@@ -251,32 +233,38 @@ export default class GithubPackage {
if (!activeItem && this.project.getPaths().length === 1) {
nextActiveProjectPath = this.project.getPaths()[0];
nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
+ nextActiveResolutionProgress = await this.getResolutionProgressForWorkdirPath(nextActiveProjectPath);
}
- if (nextActiveRepository && nextActiveRepository !== this.activeRepository) {
- this.activeProjectPath = nextActiveProjectPath || null;
- this.setActiveModels(nextActiveRepository, nextActiveResolutionProgress);
+ this.setActiveModels(nextActiveProjectPath, nextActiveRepository, nextActiveResolutionProgress);
+ }
+
+ setActiveModels(projectPath, repository, resolutionProgress) {
+ // Ensure that models are updated again when a non-null repository is destroyed
+ if (repository) {
+ if (this.destroyedRepositorySubscription) {
+ this.destroyedRepositorySubscription.dispose();
+ }
- if (this.destroyedRepositorySubscription) { this.destroyedRepositorySubscription.dispose(); }
- this.destroyedRepositorySubscription = nextActiveRepository.onDidDestroy(() => {
- if (nextActiveRepository === this.activeRepository) {
- this.setActiveModels(null);
+ this.destroyedRepositorySubscription = repository.onDidDestroy(() => {
+ if (repository === this.activeRepository) {
+ this.setActiveModels(null, null, null);
}
this.destroyedRepositorySubscription.dispose();
});
- } else if (nextActiveRepository && this.activeProjectPath !== nextActiveProjectPath) {
- this.activeProjectPath = nextActiveProjectPath || null;
- this.rerender();
- } else if (!nextActiveRepository) {
- this.activeProjectPath = nextActiveProjectPath || null;
- this.setActiveModels(null, null);
}
- }
- setActiveModels(repository, resolutionProgress) {
+ const isChanging = this.activeProjectPath !== projectPath ||
+ this.activeRepository !== repository ||
+ this.activeResolutionProgress !== resolutionProgress;
+
+ this.activeProjectPath = projectPath;
this.activeRepository = repository;
this.activeResolutionProgress = resolutionProgress;
- this.rerender();
+
+ if (isChanging) {
+ this.rerender();
+ }
}
refreshAtomGitRepository(repository) {
@@ -313,24 +301,15 @@ export default class GithubPackage {
}
cacheModelsForPaths(addedPaths) {
- const changeObserverPromises = [];
- const modelsPromises = [];
- const ready = Promise.all(addedPaths.map(async projectPath => {
+ return Promise.all(addedPaths.map(async projectPath => {
const repository = await Repository.open(projectPath);
if (!repository) {
return;
}
- const {modelPromise, changeObserverPromise} = this.addRepository(repository);
- modelsPromises.push(modelPromise);
- changeObserverPromises.push(changeObserverPromise);
+ this.addRepository(repository);
}));
-
- return {
- changeObserversStartedPromise: ready.then(() => Promise.all(changeObserverPromises)),
- modelsPromise: ready.then(() => Promise.all(modelsPromises)),
- };
}
addRepository(repository) {
@@ -373,8 +352,6 @@ export default class GithubPackage {
changeObserverPromise,
resolutionProgressPromise,
});
-
- return {modelPromise: resolutionProgressPromise, changeObserverPromise};
}
destroyModelsForPaths(removedPaths) {
From fed769c6358909e85a0a1cc75082ebafe386dd5c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Sat, 1 Apr 2017 11:37:14 -0400
Subject: [PATCH 002/161] Rework unit tests to avoid getInitialModelsPromise()
---
test/github-package.test.js | 62 +++++++++++++++++++++++++++++++++----
1 file changed, 56 insertions(+), 6 deletions(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 9d9ccfb67b..3fbcd5e9cc 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -32,6 +32,37 @@ describe('GithubPackage', function() {
atomEnv.destroy();
});
+ // Create a Promise that will resolve successfully when all models for all expected working directories have been
+ // initialized.
+ function untilModelsReady(workdirs, models = ['respository', 'changeObserver', 'resolutionProgress'], pkg = githubPackage) {
+ const modelPromiseGetters = models.map(modelName => {
+ return `get${modelName[0].toUpperCase() + modelName.slice(1)}ForWorkdirPath`;
+ });
+
+ const modelReadyForWorkdir = (getter, workdir) => new Promise(resolve => {
+ const attempt = () => {
+ pkg[getter](workdir).then(result => {
+ if (result === null) {
+ setTimeout(attempt, 100);
+ } else {
+ resolve();
+ }
+ });
+ };
+ attempt();
+ });
+
+ const readyPromises = [];
+ for (let i = 0; i < workdirs.length; i++) {
+ const workdir = workdirs[i];
+ for (let j = 0; j < modelPromiseGetters.length; j++) {
+ const getter = modelPromiseGetters[j];
+ readyPromises.push(modelReadyForWorkdir(getter, workdir));
+ }
+ }
+ return Promise.all(readyPromises);
+ }
+
describe('activate()', function() {
it('updates the active repository', async function() {
await githubPackage.activate();
@@ -88,7 +119,8 @@ describe('GithubPackage', function() {
const workdirPath3 = await cloneRepository('three-files');
project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
await githubPackage.activate();
- await githubPackage.getInitialModelsPromise();
+
+ await untilModelsReady([workdirPath1, workdirPath2, workdirPath3], ['repository']);
const repository1 = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
const repository2 = await githubPackage.getRepositoryForWorkdirPath(workdirPath2);
@@ -204,7 +236,7 @@ describe('GithubPackage', function() {
project.setPaths([workdirMergeConflict, workdirNoConflict, nonRepositoryPath]);
await githubPackage.activate();
- await githubPackage.getInitialModelsPromise();
+ await untilModelsReady([workdirMergeConflict, workdirNoConflict], ['resolutionProgress']);
// Open a file in the merge conflict repository.
await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-ours.txt'));
@@ -241,12 +273,13 @@ describe('GithubPackage', function() {
// Don't worry about this on Windows as it's not a common op
if (process.platform !== 'win32') {
- it('handles symlinked project paths', async () => {
+ it('handles symlinked project paths', async function() {
const workdirPath = await cloneRepository('three-files');
const symlinkPath = fs.realpathSync(temp.mkdirSync()) + '-symlink';
fs.symlinkSync(workdirPath, symlinkPath);
project.setPaths([symlinkPath]);
await githubPackage.activate();
+ await untilModelsReady([workdirPath], ['repository']);
await workspace.open(path.join(symlinkPath, 'a.txt'));
@@ -269,7 +302,24 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
- await githubPackage.getInitialWatchersStartedPromise();
+
+ // Wait for all change observers to be present.
+ const changeObserverForWorkdir = workdirPath => new Promise(resolve => {
+ const attempt = () => {
+ githubPackage.getChangeObserverForWorkdirPath(workdirPath).then(result => {
+ if (result === null) {
+ setTimeout(attempt, 100);
+ } else {
+ resolve();
+ }
+ });
+ };
+ attempt();
+ });
+ await Promise.all(
+ [workdirPath1, workdirPath2].map(workdirPath => changeObserverForWorkdir(workdirPath)),
+ );
+
[atomGitRepository1, atomGitRepository2] = githubPackage.project.getRepositories();
sinon.stub(atomGitRepository1, 'refreshStatus');
sinon.stub(atomGitRepository2, 'refreshStatus');
@@ -356,7 +406,7 @@ describe('GithubPackage', function() {
project.setPaths([workdirMergeConflict, workdirNoConflict]);
await githubPackage.activate();
- await githubPackage.getInitialModelsPromise();
+ await untilModelsReady([workdirMergeConflict, workdirNoConflict], ['resolutionProgress']);
// Record some state to recover later.
const resolutionMergeConflict0 = await githubPackage.getResolutionProgressForWorkdirPath(workdirMergeConflict);
@@ -373,7 +423,7 @@ describe('GithubPackage', function() {
workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm,
);
await githubPackage1.activate(payload);
- await githubPackage1.getInitialModelsPromise();
+ await untilModelsReady([workdirMergeConflict, workdirNoConflict], ['resolutionProgress'], githubPackage1);
const resolutionMergeConflict1 = await githubPackage1.getResolutionProgressForWorkdirPath(workdirMergeConflict);
const resolutionNoConflict1 = await githubPackage1.getResolutionProgressForWorkdirPath(workdirNoConflict);
From 909b6033ab4f1c658c101af0e3f03cca3bc60200 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 5 Apr 2017 10:57:08 -0400
Subject: [PATCH 003/161] (temporary) console.logstravaganza
---
lib/github-package.js | 4 ++++
test/github-package.test.js | 1 +
2 files changed, 5 insertions(+)
diff --git a/lib/github-package.js b/lib/github-package.js
index 8bee860750..b65ecf41b9 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -190,6 +190,7 @@ export default class GithubPackage {
@autobind
didChangeActivePaneItem() {
+ console.log('didChangeActivePaneItem triggered');
this.updateActiveModels();
}
@@ -211,14 +212,17 @@ export default class GithubPackage {
let nextActiveProjectPath = null;
const activeItem = this.workspace.getActivePaneItem();
+ console.log(`updateActiveModels: activeItem is ${!!activeItem} and activeItem.getPath is ${activeItem && typeof activeItem.getPath}`);
if (activeItem && typeof activeItem.getPath === 'function') {
const projectPath = this.projectPathForItemPath(activeItem.getPath());
+ console.log(`updateActiveModels: projectPath is ${projectPath}`);
if (projectPath) {
nextActiveProjectPath = projectPath;
await Promise.all([
this.getRepositoryForWorkdirPath(projectPath).then(r => { nextActiveRepository = r; }),
this.getResolutionProgressForWorkdirPath(projectPath).then(rp => { nextActiveResolutionProgress = rp; }),
]);
+ console.log(`updateActiveModels: nextActiveRepository is ${nextActiveRepository} nextActiveResolutionProgress is ${nextActiveResolutionProgress}`);
}
} else {
nextActiveRepository = this.activeRepository;
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 3fbcd5e9cc..d4f147bf14 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -107,6 +107,7 @@ describe('GithubPackage', function() {
await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath2);
await assert.async.equal(githubPackage.rerender.callCount, 3);
+ console.log('--- About to open c.txt ---');
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
await assert.async.isNull(githubPackage.getActiveRepository());
await assert.async.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
From bf1cd091e9e09cffa6e62c970ddbbbf4300dd075 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 5 Apr 2017 14:10:45 -0400
Subject: [PATCH 004/161] Use an AsyncQueue for model updates
---
lib/github-package.js | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index b65ecf41b9..de48a106e9 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -16,6 +16,7 @@ import FilePatchController from './controllers/file-patch-controller';
import RootController from './controllers/root-controller';
import IssueishPaneItem from './atom-items/issueish-pane-item';
import GitTimingsView from './views/git-timings-view';
+import AsyncQueue from './async-queue';
const defaultState = {
resolutionProgressByPath: {},
@@ -36,6 +37,7 @@ export default class GithubPackage {
this.modelsByProjectPath = new Map();
this.confirm = confirm;
+ this.activeModelQueue = new AsyncQueue();
this.activeProjectPath = null;
this.activeRepository = null;
this.activeResolutionProgress = null;
@@ -167,7 +169,7 @@ export default class GithubPackage {
this.cacheModelsForPaths(Array.from(added)),
this.destroyModelsForPaths(Array.from(removed)),
]);
- await this.updateActiveModels();
+ await this.scheduleActiveModelUpdate();
}
@autobind
@@ -177,7 +179,7 @@ export default class GithubPackage {
}
const repository = await Repository.init(projectPath);
this.addRepository(repository);
- await this.updateActiveModels();
+ await this.scheduleActiveModelUpdate();
}
@autobind
@@ -185,13 +187,12 @@ export default class GithubPackage {
const repository = await Repository.clone(remoteUrl, projectPath);
this.addRepository(repository);
this.project.addPath(projectPath);
- await this.updateActiveModels();
+ await this.scheduleActiveModelUpdate();
}
@autobind
- didChangeActivePaneItem() {
- console.log('didChangeActivePaneItem triggered');
- this.updateActiveModels();
+ async didChangeActivePaneItem() {
+ await this.scheduleActiveModelUpdate();
}
getActiveProjectPath() {
@@ -206,23 +207,25 @@ export default class GithubPackage {
return this.activeResolutionProgress;
}
+ async scheduleActiveModelUpdate() {
+ await this.activeModelQueue.push(this.updateActiveModels, {parallel: false});
+ }
+
+ @autobind
async updateActiveModels() {
let nextActiveRepository = null;
let nextActiveResolutionProgress = null;
let nextActiveProjectPath = null;
const activeItem = this.workspace.getActivePaneItem();
- console.log(`updateActiveModels: activeItem is ${!!activeItem} and activeItem.getPath is ${activeItem && typeof activeItem.getPath}`);
if (activeItem && typeof activeItem.getPath === 'function') {
const projectPath = this.projectPathForItemPath(activeItem.getPath());
- console.log(`updateActiveModels: projectPath is ${projectPath}`);
if (projectPath) {
nextActiveProjectPath = projectPath;
await Promise.all([
this.getRepositoryForWorkdirPath(projectPath).then(r => { nextActiveRepository = r; }),
this.getResolutionProgressForWorkdirPath(projectPath).then(rp => { nextActiveResolutionProgress = rp; }),
]);
- console.log(`updateActiveModels: nextActiveRepository is ${nextActiveRepository} nextActiveResolutionProgress is ${nextActiveResolutionProgress}`);
}
} else {
nextActiveRepository = this.activeRepository;
From a1d9fca4fbddc199e0f68b5b15b89182e9b09cdf Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 5 Apr 2017 14:11:42 -0400
Subject: [PATCH 005/161] Use .scheduleActiveModelUpdate()
---
test/github-package.test.js | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index d4f147bf14..365ad82ff3 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -176,43 +176,43 @@ describe('GithubPackage', function() {
await workspace.open(path.join(workdirPath1, 'a.txt'));
await workspace.open(path.join(workdirPath2, 'b.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.isNotNull(githubPackage.getActiveRepository());
assert.equal(githubPackage.getActiveProjectPath(), workdirPath2);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
assert.isNull(githubPackage.getActiveRepository());
await workspace.open(path.join(workdirPath1, 'a.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.equal(githubPackage.getActiveProjectPath(), workdirPath1);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
workspace.getActivePane().activateItem({}); // such as when find & replace results pane is focused
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.equal(githubPackage.getActiveProjectPath(), workdirPath1);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
await workspace.open(path.join(workdirPath2, 'b.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.equal(githubPackage.getActiveProjectPath(), workdirPath2);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
project.removePath(workdirPath2);
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
project.removePath(workdirPath1);
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
await workspace.open(path.join(workdirPath1, 'a.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
});
@@ -222,7 +222,7 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath]);
await githubPackage.activate();
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
const repository = await githubPackage.getRepositoryForWorkdirPath(workdirPath);
await assert.async.strictEqual(githubPackage.getActiveRepository(), repository);
@@ -241,7 +241,7 @@ describe('GithubPackage', function() {
// Open a file in the merge conflict repository.
await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-ours.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
const resolutionMergeConflict = await githubPackage.getResolutionProgressForWorkdirPath(workdirMergeConflict);
await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
@@ -251,7 +251,7 @@ describe('GithubPackage', function() {
// Open a file in the non-merge conflict repository.
await workspace.open(path.join(workdirNoConflict, 'b.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
const resolutionNoConflict = await githubPackage.getResolutionProgressForWorkdirPath(workdirNoConflict);
await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionNoConflict);
@@ -259,13 +259,13 @@ describe('GithubPackage', function() {
// Open a file in the workdir with no repository.
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
await assert.isNull(githubPackage.getActiveResolutionProgress());
// Re-open a file in the merge conflict repository.
await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-theirs.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
assert.isFalse(githubPackage.getActiveResolutionProgress().isEmpty());
@@ -284,7 +284,7 @@ describe('GithubPackage', function() {
await workspace.open(path.join(symlinkPath, 'a.txt'));
- await githubPackage.updateActiveModels();
+ await githubPackage.scheduleActiveModelUpdate();
assert.isOk(githubPackage.getActiveRepository());
});
}
From fc4e67822318153e3a63f7e247d929272f4c55cd Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 5 Apr 2017 14:16:18 -0400
Subject: [PATCH 006/161] :fire: last console.log
---
test/github-package.test.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 365ad82ff3..3d5e61114b 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -107,7 +107,6 @@ describe('GithubPackage', function() {
await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath2);
await assert.async.equal(githubPackage.rerender.callCount, 3);
- console.log('--- About to open c.txt ---');
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
await assert.async.isNull(githubPackage.getActiveRepository());
await assert.async.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
From 872f5c0e0a15c27e8fc98574edb05145a0d23159 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 6 Apr 2017 10:57:06 -0400
Subject: [PATCH 007/161] Introduce a WorkdirContext to bundle workdir-specific
models
---
lib/models/workdir-context.js | 176 ++++++++++++++++++++++++++++
test/models/workdir-context.test.js | 154 ++++++++++++++++++++++++
2 files changed, 330 insertions(+)
create mode 100644 lib/models/workdir-context.js
create mode 100644 test/models/workdir-context.test.js
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
new file mode 100644
index 0000000000..0838254680
--- /dev/null
+++ b/lib/models/workdir-context.js
@@ -0,0 +1,176 @@
+import {Emitter, CompositeDisposable} from 'atom'; // FIXME import from event-kit
+
+import Repository from './repository';
+import ResolutionProgress from './conflicts/resolution-progress';
+import FileSystemChangeObserver from './file-system-change-observer';
+import WorkspaceChangeObserver from './workspace-change-observer';
+
+// Provide both synchronous and asynchronous access to a value.
+class Deferred {
+ constructor(value = null) {
+ if (value !== null) {
+ this.value = value;
+ this.promise = Promise.resolve(value);
+ this.resolve = () => {
+ throw new Error('Attempt to resolve Deferred with concrete value');
+ };
+ this.reject = () => {
+ throw new Error('Attempt to reject Deferred with concrete value');
+ };
+ } else {
+ this.value = null;
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ }
+ }
+
+ hasValue() {
+ return this.value !== null;
+ }
+
+ setValue(value) {
+ this.value = value;
+ this.resolve(value);
+ }
+
+ getValue() {
+ return this.value;
+ }
+
+ getPromise() {
+ return this.promise;
+ }
+}
+
+/*
+ * Bundle of model objects associated with a git working directory.
+ *
+ * Provides synchronous access to each model in the form of a getter method that returns the model or `null` if it
+ * has not yet been initialized, and asynchronous access in the form of a Promise generation method that will resolve
+ * once the model is available. Initializes the platform-appropriate change observer and proxies select filesystem
+ * change events.
+ */
+export default class WorkdirContext {
+
+ /*
+ * Available options:
+ * - `options.repository`: Pre-initialized Repository instance if one is available.
+ * - `options.savedResolutionProgress`: Serialized state used to initialize the ResolutionProgress.
+ * - `options.window`: Browser window global, used on Linux by the WorkspaceChangeObserver.
+ * - `options.workspace`: Atom's workspace singleton, used on Linux by the WorkspaceChangeObserver.
+ */
+ constructor(directory, options = {}) {
+ this.directory = directory;
+ this.options = options;
+
+ this.repositoryHolder = new Deferred(this.options.repository || null);
+ this.changeObserverHolder = new Deferred();
+ this.resolutionProgressHolder = new Deferred();
+
+ this.destroyed = false;
+ this.emitter = new Emitter();
+ this.subs = new CompositeDisposable();
+ }
+
+ /*
+ * Initialize all model objects associated with this git working directory. Return a Promise that will resolve when
+ * all models are available and ready, including the filesystem watcher. Outside of test cases, it's generally
+ * better to not await this Promise and instead await the `getXyzPromise()` of the models that you need.
+ */
+ async create() {
+ if (this.destroyed) {
+ return;
+ }
+
+ let repo;
+ if (!this.repositoryHolder.hasValue()) {
+ repo = await Repository.open(this.directory);
+ this.repositoryHolder.setValue(repo);
+ } else {
+ repo = this.getRepository();
+ }
+
+ const changeObserver = process.platform === 'linux'
+ ? new WorkspaceChangeObserver(this.options.window, this.options.workspace, repo)
+ : new FileSystemChangeObserver(repo);
+
+ this.subs.add(changeObserver.onDidChange(() => repo.refresh()));
+ this.subs.add(changeObserver.onDidChangeWorkdirOrHead(() => this.emitter.emit('did-change-workdir-or-head')));
+
+ this.changeObserverHolder.setValue(changeObserver);
+
+ const populateResolutionProgress = async () => {
+ const commit = await repo.getLastCommit();
+ const progress = new ResolutionProgress(commit.sha, this.options.savedResolutionProgress || {});
+ this.resolutionProgressHolder.setValue(progress);
+ };
+
+ await Promise.all([
+ populateResolutionProgress(),
+ changeObserver.start(),
+ ]);
+ }
+
+ isDestroyed() {
+ return this.destroyed;
+ }
+
+ /*
+ * Subscribe to the filesystem observer event.
+ */
+ onDidChangeWorkdirOrHead(callback) {
+ return this.emitter.on('did-change-workdir-or-head', callback);
+ }
+
+ getRepository() {
+ return this.repositoryHolder.getValue();
+ }
+
+ getRepositoryPromise() {
+ return this.repositoryHolder.getPromise();
+ }
+
+ getChangeObserver() {
+ return this.changeObserverHolder.getValue();
+ }
+
+ getChangeObserverPromise() {
+ return this.changeObserverHolder.getPromise();
+ }
+
+ getResolutionProgress() {
+ return this.resolutionProgressHolder.getValue();
+ }
+
+ getResolutionProgressPromise() {
+ return this.resolutionProgressHolder.getPromise();
+ }
+
+ /*
+ * Cleanly destroy any models that need to be cleaned, including stopping the filesystem watcher.
+ */
+ async destroy() {
+ if (this.destroyed) {
+ return;
+ }
+
+ this.destroyed = true;
+ this.subs.dispose();
+ this.emitter.dispose();
+
+ const observer = this.getChangeObserver();
+ const repository = this.getRepository();
+
+ const promises = [];
+ if (observer) {
+ promises.push(observer.destroy());
+ }
+ if (repository) {
+ promises.push(repository.destroy());
+ }
+
+ await Promise.all(promises);
+ }
+}
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
new file mode 100644
index 0000000000..9586266427
--- /dev/null
+++ b/test/models/workdir-context.test.js
@@ -0,0 +1,154 @@
+import {CompositeDisposable} from 'atom';
+
+import {cloneRepository} from '../helpers';
+
+import WorkdirContext from '../../lib/models/workdir-context';
+import Repository from '../../lib/models/repository';
+
+describe('WorkdirContext', function() {
+ let context, workingDirectory, subs;
+
+ beforeEach(async function() {
+ workingDirectory = await cloneRepository('three-files');
+ subs = new CompositeDisposable();
+ });
+
+ afterEach(async function() {
+ context && await context.destroy();
+ subs.dispose();
+ });
+
+ describe('without an initial Repository', function() {
+ beforeEach(function() {
+ context = new WorkdirContext(workingDirectory);
+ });
+
+ it('returns null for each synchronous model', function() {
+ assert.isNull(context.getRepository());
+ assert.isNull(context.getResolutionProgress());
+ assert.isNull(context.getChangeObserver());
+ });
+
+ it('populates the repository on create()', async function() {
+ const repoPromise = context.getRepositoryPromise();
+
+ await context.create();
+
+ const repo = context.getRepository();
+ assert.isNotNull(repo);
+ assert.equal(repo.getWorkingDirectoryPath(), workingDirectory);
+ assert.strictEqual(repo, await repoPromise);
+ });
+
+ it('populates the resolution progress on create()', async function() {
+ const progressPromise = context.getResolutionProgressPromise();
+
+ await context.create();
+
+ const progress = context.getResolutionProgress();
+ assert.isNotNull(progress);
+ assert.strictEqual(progress, await progressPromise);
+ });
+
+ it('populates the change observer on create()', async function() {
+ const observerPromise = context.getChangeObserverPromise();
+
+ await context.create();
+
+ const observer = context.getChangeObserver();
+ assert.isNotNull(observer);
+ assert.strictEqual(observer, await observerPromise);
+ });
+
+ it('refreshes the repository on any filesystem change', async function() {
+ await context.create();
+
+ const repo = context.getRepository();
+ sinon.spy(repo, 'refresh');
+
+ const observer = context.getChangeObserver();
+ observer.emitter.emit('did-change');
+
+ assert.isTrue(repo.refresh.called);
+ });
+
+ it('emits an event on workdir or head change', async function() {
+ const listener = sinon.spy();
+ subs.add(context.onDidChangeWorkdirOrHead(listener));
+
+ await context.create();
+
+ context.getChangeObserver().emitter.emit('did-change-workdir-or-head');
+ assert.isTrue(listener.called);
+ });
+
+ it('can be destroyed before create()', async function() {
+ await context.destroy();
+ assert.isTrue(context.isDestroyed());
+
+ await context.create();
+ assert.isTrue(context.isDestroyed());
+ });
+
+ it('destroys the repository on destroy()', async function() {
+ await context.create();
+ const repo = context.getRepository();
+ sinon.spy(repo, 'destroy');
+
+ await context.destroy();
+ assert.isTrue(repo.destroy.called);
+ });
+
+ it('stops the change observer on destroy()', async function() {
+ await context.create();
+ const observer = context.getChangeObserver();
+ await observer.start();
+
+ sinon.spy(observer, 'destroy');
+
+ await context.destroy();
+ assert.isTrue(observer.destroy.called);
+ });
+
+ it('can be destroyed twice', async function() {
+ await context.create();
+ assert.isFalse(context.isDestroyed());
+
+ await context.destroy();
+ assert.isTrue(context.isDestroyed());
+ });
+ });
+
+ describe('with an initial Repository', function() {
+ let repository;
+
+ beforeEach(async function() {
+ repository = await Repository.open(workingDirectory);
+ context = new WorkdirContext(workingDirectory, {repository});
+ });
+
+ it('returns the repository synchrounsly', function() {
+ assert.strictEqual(repository, context.getRepository());
+ });
+
+ it('populates the resolution progress on create()', async function() {
+ const progressPromise = context.getResolutionProgressPromise();
+
+ await context.create();
+
+ const progress = context.getResolutionProgress();
+ assert.isNotNull(progress);
+ assert.strictEqual(progress, await progressPromise);
+ });
+
+ it('populates the change observer on create()', async function() {
+ const observerPromise = context.getChangeObserverPromise();
+
+ await context.create();
+
+ const observer = context.getChangeObserver();
+ assert.isNotNull(observer);
+ assert.strictEqual(observer, await observerPromise);
+ });
+ });
+});
From 765bf0efd4fc3f6dc8b08655131a45741fd446ba Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 6 Apr 2017 16:15:23 -0400
Subject: [PATCH 008/161] Repository.destroy() is synchronous
---
lib/models/workdir-context.js | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 0838254680..70d8b42f95 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -163,14 +163,10 @@ export default class WorkdirContext {
const observer = this.getChangeObserver();
const repository = this.getRepository();
- const promises = [];
+ repository && repository.destroy();
+
if (observer) {
- promises.push(observer.destroy());
- }
- if (repository) {
- promises.push(repository.destroy());
+ await observer.destroy();
}
-
- await Promise.all(promises);
}
}
From 1ec5774039ae34459a3ef26777cd3258f9c35683 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 6 Apr 2017 16:16:51 -0400
Subject: [PATCH 009/161] WorkdirContextPool to manage the set of
WorkdirContexts
---
lib/models/workdir-context-pool.js | 58 ++++++++++
test/models/workdir-context-pool.test.js | 130 +++++++++++++++++++++++
2 files changed, 188 insertions(+)
create mode 100644 lib/models/workdir-context-pool.js
create mode 100644 test/models/workdir-context-pool.test.js
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
new file mode 100644
index 0000000000..af32e19d1f
--- /dev/null
+++ b/lib/models/workdir-context-pool.js
@@ -0,0 +1,58 @@
+import compareSets from 'compare-sets';
+
+import WorkdirContext from './workdir-context';
+
+/**
+ * Manage a WorkdirContext for each open directory.
+ */
+export default class WorkdirContextPool {
+
+ /**
+ * Options will be passed to each `WorkdirContext` as it is created.
+ */
+ constructor(options = {}) {
+ this.options = options;
+
+ this.contexts = new Map();
+ }
+
+ size() {
+ return this.contexts.size;
+ }
+
+ /**
+ * Access the context mapped to a known directory.
+ */
+ getContext(directory) {
+ return this.contexts.get(directory);
+ }
+
+ add(directory, options = {}) {
+ if (this.contexts.has(directory)) {
+ return;
+ }
+
+ const context = new WorkdirContext(directory, {...this.options, ...options});
+ this.contexts.set(directory, context);
+
+ context.create();
+ }
+
+ remove(directory) {
+ const existing = this.contexts.get(directory);
+ this.contexts.delete(directory);
+ existing && existing.destroy();
+ }
+
+ set(directories) {
+ const previous = new Set(this.contexts.keys());
+ const {added, removed} = compareSets(previous, directories);
+
+ for (const directory of added) {
+ this.add(directory);
+ }
+ for (const directory of removed) {
+ this.remove(directory);
+ }
+ }
+}
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
new file mode 100644
index 0000000000..2ea6ceb6b2
--- /dev/null
+++ b/test/models/workdir-context-pool.test.js
@@ -0,0 +1,130 @@
+import {cloneRepository} from '../helpers';
+
+import Repository from '../../lib/models/repository';
+import WorkdirContextPool from '../../lib/models/workdir-context-pool';
+
+describe('WorkdirContextPool', function() {
+ let atomEnv, workspace, pool;
+
+ beforeEach(function() {
+ atomEnv = global.buildAtomEnvironment();
+ workspace = atomEnv.workspace;
+
+ pool = new WorkdirContextPool({
+ window, workspace,
+ });
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ });
+
+ describe('add', function() {
+ let workingDirectory;
+
+ beforeEach(async function() {
+ workingDirectory = await cloneRepository('three-files');
+ });
+
+ it('adds a WorkdirContext for a new working directory', function() {
+ assert.equal(pool.size(), 0);
+ assert.isUndefined(pool.getContext(workingDirectory));
+
+ pool.add(workingDirectory);
+
+ assert.equal(pool.size(), 1);
+ assert.isDefined(pool.getContext(workingDirectory));
+ });
+
+ it('optionally provides a preinitialized repository', async function() {
+ const existingRepo = await Repository.open(workingDirectory);
+
+ pool.add(workingDirectory, {repository: existingRepo});
+
+ const addedRepo = pool.getContext(workingDirectory).getRepository();
+ assert.strictEqual(addedRepo, existingRepo);
+ });
+
+ it('is a no-op if the working directory already has a context', function() {
+ pool.add(workingDirectory);
+ assert.equal(pool.size(), 1);
+
+ const context = pool.getContext(workingDirectory);
+ assert.isDefined(context);
+
+ pool.add(workingDirectory);
+ assert.equal(pool.size(), 1);
+ });
+
+ it('begins but does not await the asynchronous initialization process', async function() {
+ pool.add(workingDirectory);
+ const context = pool.getContext(workingDirectory);
+ assert.isNull(context.getRepository());
+
+ const repo = await context.getRepositoryPromise();
+ assert.isNotNull(repo);
+ });
+ });
+
+ describe('remove', function() {
+ let existingDirectory, existingContext;
+
+ beforeEach(async function() {
+ existingDirectory = await cloneRepository('three-files');
+ pool.add(existingDirectory);
+
+ existingContext = pool.getContext(existingDirectory);
+ });
+
+ it('removes a WorkdirContext for an existing working directory', function() {
+ assert.equal(pool.size(), 1);
+ pool.remove(existingDirectory);
+ assert.isUndefined(pool.getContext(existingDirectory));
+ assert.equal(pool.size(), 0);
+ });
+
+ it('is a no-op if the working directory is not present', function() {
+ assert.equal(pool.size(), 1);
+ pool.remove('/nope');
+ assert.equal(pool.size(), 1);
+ });
+
+ it('begins but does not await the termination process', async function() {
+ const repo = await existingContext.getRepositoryPromise();
+ sinon.spy(repo, 'destroy');
+
+ assert.isFalse(existingContext.isDestroyed());
+ pool.remove(existingDirectory);
+ assert.isTrue(existingContext.isDestroyed());
+ assert.isTrue(repo.destroy.called);
+ });
+ });
+
+ describe('set', function() {
+ let dir0, dir1, dir2;
+
+ beforeEach(async function() {
+ [dir0, dir1, dir2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+
+ pool.add(dir0);
+ pool.add(dir1);
+ });
+
+ it('adds new directories, removes missing ones, and maintains kept ones', function() {
+ const context0 = pool.getContext(dir0);
+ const context1 = pool.getContext(dir1);
+
+ pool.set([dir1, dir2]);
+
+ assert.equal(pool.size(), 2);
+
+ assert.isTrue(context0.isDestroyed());
+ assert.isFalse(context1.isDestroyed());
+ assert.isDefined(pool.getContext(dir2));
+ });
+ });
+});
From 3cbfefb74b0fcda4ca4ea49f4f1ca872a3acdbdf Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 07:52:30 -0400
Subject: [PATCH 010/161] (remove) :checkered_flag: Windows day
:checkered_flag:
---
lib/github-package.js | 83 ++++++++++++----------
lib/models/workdir-context.js | 14 ++++
test/github-package.test.js | 89 ++++++++++++++++++++++--
test/models/workdir-context-pool.test.js | 37 ++++++++--
4 files changed, 174 insertions(+), 49 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index de48a106e9..ab59897f22 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -83,8 +83,6 @@ export default class GithubPackage {
this.workspace.addOpener(IssueishPaneItem.opener),
);
- this.deserialize(this.savedState);
-
this.rerender();
}
@@ -106,12 +104,6 @@ export default class GithubPackage {
};
}
- async deserialize(state) {
- if (state.activeRepositoryPath) {
- await this.didChangeProjectPaths([state.activeRepositoryPath]);
- }
- }
-
@autobind
rerender() {
if (!this.element) {
@@ -207,44 +199,61 @@ export default class GithubPackage {
return this.activeResolutionProgress;
}
- async scheduleActiveModelUpdate() {
- await this.activeModelQueue.push(this.updateActiveModels, {parallel: false});
+ async scheduleActiveModelUpdate(isActivating = false) {
+ await this.activeModelQueue.push(this.updateActiveModels.bind(this, isActivating), {parallel: false});
}
- @autobind
- async updateActiveModels() {
- let nextActiveRepository = null;
- let nextActiveResolutionProgress = null;
- let nextActiveProjectPath = null;
+ async updateActiveModels(isActivating = false) {
+ let foundActiveModels = false;
+
+ let nextActiveRepository = this.activeRepository;
+ let nextActiveResolutionProgress = this.activeResolutionProgress;
+ let nextActiveProjectPath = this.activeProjectPath;
+
+ const setByProjectPath = async projectPath => {
+ if (!projectPath) {
+ return;
+ }
+
+ nextActiveProjectPath = projectPath;
+ nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
+ nextActiveResolutionProgress = await this.getResolutionProgressForWorkdirPath(nextActiveProjectPath);
+
+ foundActiveModels = true;
+ };
const activeItem = this.workspace.getActivePaneItem();
- if (activeItem && typeof activeItem.getPath === 'function') {
- const projectPath = this.projectPathForItemPath(activeItem.getPath());
- if (projectPath) {
- nextActiveProjectPath = projectPath;
- await Promise.all([
- this.getRepositoryForWorkdirPath(projectPath).then(r => { nextActiveRepository = r; }),
- this.getResolutionProgressForWorkdirPath(projectPath).then(rp => { nextActiveResolutionProgress = rp; }),
- ]);
+
+ if (activeItem) {
+ if (typeof activeItem.getRepository === 'function') {
+ // FilePatchController or other GitHub package pane
+ const repository = activeItem.getRepository();
+ if (!repository.isDestroyed()) {
+ nextActiveRepository = repository;
+ nextActiveResolutionProgress = this.props.activeResolutionProgress;
+ nextActiveProjectPath = this.props.activeProjectPath;
+
+ foundActiveModels = true;
+ }
+
+ } else if (typeof activeItem.getPath === 'function') {
+ // TextEditor-like
+ const projectPath = this.projectPathForItemPath(activeItem.getPath());
+ await setByProjectPath(projectPath);
}
- } else {
- nextActiveRepository = this.activeRepository;
- nextActiveResolutionProgress = this.activeResolutionProgress;
- nextActiveProjectPath = this.activeProjectPath;
}
- if (activeItem instanceof FilePatchController) {
- if (!activeItem.getRepository().isDestroyed()) {
- nextActiveRepository = activeItem.props.repository;
- nextActiveResolutionProgress = activeItem.props.resolutionProgress;
- nextActiveProjectPath = nextActiveRepository.getWorkingDirectoryPath();
- }
+ if (!foundActiveModels && this.project.getPaths().length === 1) {
+ // Single project
+ await setByProjectPath(this.project.getPaths()[0]);
}
- if (!activeItem && this.project.getPaths().length === 1) {
- nextActiveProjectPath = this.project.getPaths()[0];
- nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
- nextActiveResolutionProgress = await this.getResolutionProgressForWorkdirPath(nextActiveProjectPath);
+ if (!foundActiveModels && isActivating) {
+ // Restore models from saved state
+ if (this.savedState.activeRepositoryPath) {
+ const projectPath = this.projectPathForItemPath(this.savedState.activeRepositoryPath);
+ await setByProjectPath(projectPath);
+ }
}
this.setActiveModels(nextActiveProjectPath, nextActiveRepository, nextActiveResolutionProgress);
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 70d8b42f95..0436e46c44 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -113,6 +113,10 @@ export default class WorkdirContext {
]);
}
+ isPresent() {
+ return true;
+ }
+
isDestroyed() {
return this.destroyed;
}
@@ -170,3 +174,13 @@ export default class WorkdirContext {
}
}
}
+
+class NullWorkdirContext extends WorkdirContext {
+ constructor() {
+ super(null, {});
+
+ this.repositoryHolder.setValue(null);
+ this.changeObserverHolder.setValue(null);
+ this.resolutionProgressHolder.setValue(null);
+ }
+}
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 3d5e61114b..0d8db5662d 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -64,17 +64,94 @@ describe('GithubPackage', function() {
}
describe('activate()', function() {
- it('updates the active repository', async function() {
+ it('uses models from preexisting projects', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1, workdirPath2]);
+
+ await githubPackage.activate();
+
+ await assert.async.isNotNull(await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
+ await assert.async.isNotNull(await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
+ assert.isNull(githubPackage.getActiveRepository());
+ });
+
+ it('uses an active model from a single preexisting project', async function() {
+ const workdirPath = await cloneRepository('three-files');
+ project.setPaths([workdirPath]);
+
await githubPackage.activate();
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath,
+ );
+ });
+
+ it('uses an active model from a preexisting active pane item', async function() {
const workdirPath1 = await cloneRepository('three-files');
const workdirPath2 = await cloneRepository('three-files');
project.setPaths([workdirPath1, workdirPath2]);
- fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'change 1', 'utf8');
- fs.writeFileSync(path.join(workdirPath1, 'b.txt'), 'change 2', 'utf8');
+ await workspace.open(path.join(workdirPath2, 'a.txt'));
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- const repository = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
- await until(() => githubPackage.getActiveRepository() === repository);
+ await githubPackage.activate();
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath2,
+ );
+ });
+
+ it('uses an active model from serialized state', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ const workdirPath3 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
+
+ await githubPackage.activate({
+ activeRepositoryPath: workdirPath2,
+ });
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath2,
+ );
+ });
+
+ it('prefers the active model from an active pane item to serialized state', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1, workdirPath2]);
+ await workspace.open(path.join(workdirPath2, 'b.txt'));
+
+ await githubPackage.activate({
+ activeRepositoryPath: workdirPath1,
+ });
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath2,
+ );
+ });
+
+ it('prefers the active model from a single project to serialized state', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1]);
+
+ await githubPackage.activate({
+ activeRepositoryPath: workdirPath2,
+ });
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath1,
+ );
});
});
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index 2ea6ceb6b2..cb2757b2d3 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -3,7 +3,7 @@ import {cloneRepository} from '../helpers';
import Repository from '../../lib/models/repository';
import WorkdirContextPool from '../../lib/models/workdir-context-pool';
-describe('WorkdirContextPool', function() {
+describe.only('WorkdirContextPool', function() {
let atomEnv, workspace, pool;
beforeEach(function() {
@@ -28,12 +28,12 @@ describe('WorkdirContextPool', function() {
it('adds a WorkdirContext for a new working directory', function() {
assert.equal(pool.size(), 0);
- assert.isUndefined(pool.getContext(workingDirectory));
+ assert.isFalse(pool.getContext(workingDirectory).isPresent());
pool.add(workingDirectory);
assert.equal(pool.size(), 1);
- assert.isDefined(pool.getContext(workingDirectory));
+ assert.isTrue(pool.getContext(workingDirectory).isPresent());
});
it('optionally provides a preinitialized repository', async function() {
@@ -50,7 +50,7 @@ describe('WorkdirContextPool', function() {
assert.equal(pool.size(), 1);
const context = pool.getContext(workingDirectory);
- assert.isDefined(context);
+ assert.isTrue(context.isPresent());
pool.add(workingDirectory);
assert.equal(pool.size(), 1);
@@ -79,7 +79,7 @@ describe('WorkdirContextPool', function() {
it('removes a WorkdirContext for an existing working directory', function() {
assert.equal(pool.size(), 1);
pool.remove(existingDirectory);
- assert.isUndefined(pool.getContext(existingDirectory));
+ assert.isFalse(pool.getContext(existingDirectory).isPresent());
assert.equal(pool.size(), 0);
});
@@ -122,9 +122,34 @@ describe('WorkdirContextPool', function() {
assert.equal(pool.size(), 2);
+ assert.isFalse(pool.getContext(dir0).isDefined());
+ assert.isTrue(pool.getContext(dir1).isDefined());
+ assert.isTrue(pool.getContext(dir2).isDefined());
+
assert.isTrue(context0.isDestroyed());
assert.isFalse(context1.isDestroyed());
- assert.isDefined(pool.getContext(dir2));
+ });
+ });
+
+ describe('getContext', function() {
+ let dir;
+
+ beforeEach(async function() {
+ dir = await cloneRepository('three-files');
+ pool.add(dir);
+ });
+
+ it('returns a context by directory', async function() {
+ const context = pool.getContext(dir);
+ assert.isTrue(context.isPresent());
+
+ const repo = await context.getRepositoryPromise();
+ assert.strictEqual(dir, repo.getWorkingDirectoryPath());
+ });
+
+ it('returns a null context when missing', function() {
+ const context = pool.getContext(dir);
+ assert.False(context.isPresent());
});
});
});
From 129d5e7eb469042463d547a33167403a05a43902 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 07:52:30 -0400
Subject: [PATCH 011/161] Return a null object from the pool when a directory
is not present
---
lib/github-package.js | 83 ++++++++++++----------
lib/models/workdir-context-pool.js | 4 +-
lib/models/workdir-context.js | 28 ++++++++
test/github-package.test.js | 89 ++++++++++++++++++++++--
test/models/workdir-context-pool.test.js | 35 ++++++++--
5 files changed, 189 insertions(+), 50 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index de48a106e9..ab59897f22 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -83,8 +83,6 @@ export default class GithubPackage {
this.workspace.addOpener(IssueishPaneItem.opener),
);
- this.deserialize(this.savedState);
-
this.rerender();
}
@@ -106,12 +104,6 @@ export default class GithubPackage {
};
}
- async deserialize(state) {
- if (state.activeRepositoryPath) {
- await this.didChangeProjectPaths([state.activeRepositoryPath]);
- }
- }
-
@autobind
rerender() {
if (!this.element) {
@@ -207,44 +199,61 @@ export default class GithubPackage {
return this.activeResolutionProgress;
}
- async scheduleActiveModelUpdate() {
- await this.activeModelQueue.push(this.updateActiveModels, {parallel: false});
+ async scheduleActiveModelUpdate(isActivating = false) {
+ await this.activeModelQueue.push(this.updateActiveModels.bind(this, isActivating), {parallel: false});
}
- @autobind
- async updateActiveModels() {
- let nextActiveRepository = null;
- let nextActiveResolutionProgress = null;
- let nextActiveProjectPath = null;
+ async updateActiveModels(isActivating = false) {
+ let foundActiveModels = false;
+
+ let nextActiveRepository = this.activeRepository;
+ let nextActiveResolutionProgress = this.activeResolutionProgress;
+ let nextActiveProjectPath = this.activeProjectPath;
+
+ const setByProjectPath = async projectPath => {
+ if (!projectPath) {
+ return;
+ }
+
+ nextActiveProjectPath = projectPath;
+ nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
+ nextActiveResolutionProgress = await this.getResolutionProgressForWorkdirPath(nextActiveProjectPath);
+
+ foundActiveModels = true;
+ };
const activeItem = this.workspace.getActivePaneItem();
- if (activeItem && typeof activeItem.getPath === 'function') {
- const projectPath = this.projectPathForItemPath(activeItem.getPath());
- if (projectPath) {
- nextActiveProjectPath = projectPath;
- await Promise.all([
- this.getRepositoryForWorkdirPath(projectPath).then(r => { nextActiveRepository = r; }),
- this.getResolutionProgressForWorkdirPath(projectPath).then(rp => { nextActiveResolutionProgress = rp; }),
- ]);
+
+ if (activeItem) {
+ if (typeof activeItem.getRepository === 'function') {
+ // FilePatchController or other GitHub package pane
+ const repository = activeItem.getRepository();
+ if (!repository.isDestroyed()) {
+ nextActiveRepository = repository;
+ nextActiveResolutionProgress = this.props.activeResolutionProgress;
+ nextActiveProjectPath = this.props.activeProjectPath;
+
+ foundActiveModels = true;
+ }
+
+ } else if (typeof activeItem.getPath === 'function') {
+ // TextEditor-like
+ const projectPath = this.projectPathForItemPath(activeItem.getPath());
+ await setByProjectPath(projectPath);
}
- } else {
- nextActiveRepository = this.activeRepository;
- nextActiveResolutionProgress = this.activeResolutionProgress;
- nextActiveProjectPath = this.activeProjectPath;
}
- if (activeItem instanceof FilePatchController) {
- if (!activeItem.getRepository().isDestroyed()) {
- nextActiveRepository = activeItem.props.repository;
- nextActiveResolutionProgress = activeItem.props.resolutionProgress;
- nextActiveProjectPath = nextActiveRepository.getWorkingDirectoryPath();
- }
+ if (!foundActiveModels && this.project.getPaths().length === 1) {
+ // Single project
+ await setByProjectPath(this.project.getPaths()[0]);
}
- if (!activeItem && this.project.getPaths().length === 1) {
- nextActiveProjectPath = this.project.getPaths()[0];
- nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
- nextActiveResolutionProgress = await this.getResolutionProgressForWorkdirPath(nextActiveProjectPath);
+ if (!foundActiveModels && isActivating) {
+ // Restore models from saved state
+ if (this.savedState.activeRepositoryPath) {
+ const projectPath = this.projectPathForItemPath(this.savedState.activeRepositoryPath);
+ await setByProjectPath(projectPath);
+ }
}
this.setActiveModels(nextActiveProjectPath, nextActiveRepository, nextActiveResolutionProgress);
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index af32e19d1f..204361b305 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -1,6 +1,6 @@
import compareSets from 'compare-sets';
-import WorkdirContext from './workdir-context';
+import WorkdirContext, {nullWorkdirContext} from './workdir-context';
/**
* Manage a WorkdirContext for each open directory.
@@ -24,7 +24,7 @@ export default class WorkdirContextPool {
* Access the context mapped to a known directory.
*/
getContext(directory) {
- return this.contexts.get(directory);
+ return this.contexts.get(directory) || nullWorkdirContext;
}
add(directory, options = {}) {
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 70d8b42f95..8ba510c41f 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -113,6 +113,10 @@ export default class WorkdirContext {
]);
}
+ isPresent() {
+ return true;
+ }
+
isDestroyed() {
return this.destroyed;
}
@@ -170,3 +174,27 @@ export default class WorkdirContext {
}
}
}
+
+class NullWorkdirContext extends WorkdirContext {
+ constructor() {
+ super(null, {});
+
+ this.repositoryHolder.setValue(null);
+ this.changeObserverHolder.setValue(null);
+ this.resolutionProgressHolder.setValue(null);
+ }
+
+ create() {
+ return Promise.resolve();
+ }
+
+ isPresent() {
+ return false;
+ }
+
+ destroy() {
+ return Promise.resolve();
+ }
+}
+
+export const nullWorkdirContext = new NullWorkdirContext();
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 3d5e61114b..0d8db5662d 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -64,17 +64,94 @@ describe('GithubPackage', function() {
}
describe('activate()', function() {
- it('updates the active repository', async function() {
+ it('uses models from preexisting projects', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1, workdirPath2]);
+
+ await githubPackage.activate();
+
+ await assert.async.isNotNull(await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
+ await assert.async.isNotNull(await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
+ assert.isNull(githubPackage.getActiveRepository());
+ });
+
+ it('uses an active model from a single preexisting project', async function() {
+ const workdirPath = await cloneRepository('three-files');
+ project.setPaths([workdirPath]);
+
await githubPackage.activate();
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath,
+ );
+ });
+
+ it('uses an active model from a preexisting active pane item', async function() {
const workdirPath1 = await cloneRepository('three-files');
const workdirPath2 = await cloneRepository('three-files');
project.setPaths([workdirPath1, workdirPath2]);
- fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'change 1', 'utf8');
- fs.writeFileSync(path.join(workdirPath1, 'b.txt'), 'change 2', 'utf8');
+ await workspace.open(path.join(workdirPath2, 'a.txt'));
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- const repository = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
- await until(() => githubPackage.getActiveRepository() === repository);
+ await githubPackage.activate();
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath2,
+ );
+ });
+
+ it('uses an active model from serialized state', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ const workdirPath3 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
+
+ await githubPackage.activate({
+ activeRepositoryPath: workdirPath2,
+ });
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath2,
+ );
+ });
+
+ it('prefers the active model from an active pane item to serialized state', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1, workdirPath2]);
+ await workspace.open(path.join(workdirPath2, 'b.txt'));
+
+ await githubPackage.activate({
+ activeRepositoryPath: workdirPath1,
+ });
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath2,
+ );
+ });
+
+ it('prefers the active model from a single project to serialized state', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ project.setPaths([workdirPath1]);
+
+ await githubPackage.activate({
+ activeRepositoryPath: workdirPath2,
+ });
+
+ await assert.async.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(
+ await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
+ workdirPath1,
+ );
});
});
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index 2ea6ceb6b2..dcdda06224 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -28,12 +28,12 @@ describe('WorkdirContextPool', function() {
it('adds a WorkdirContext for a new working directory', function() {
assert.equal(pool.size(), 0);
- assert.isUndefined(pool.getContext(workingDirectory));
+ assert.isFalse(pool.getContext(workingDirectory).isPresent());
pool.add(workingDirectory);
assert.equal(pool.size(), 1);
- assert.isDefined(pool.getContext(workingDirectory));
+ assert.isTrue(pool.getContext(workingDirectory).isPresent());
});
it('optionally provides a preinitialized repository', async function() {
@@ -50,7 +50,7 @@ describe('WorkdirContextPool', function() {
assert.equal(pool.size(), 1);
const context = pool.getContext(workingDirectory);
- assert.isDefined(context);
+ assert.isTrue(context.isPresent());
pool.add(workingDirectory);
assert.equal(pool.size(), 1);
@@ -79,7 +79,7 @@ describe('WorkdirContextPool', function() {
it('removes a WorkdirContext for an existing working directory', function() {
assert.equal(pool.size(), 1);
pool.remove(existingDirectory);
- assert.isUndefined(pool.getContext(existingDirectory));
+ assert.isFalse(pool.getContext(existingDirectory).isPresent());
assert.equal(pool.size(), 0);
});
@@ -122,9 +122,34 @@ describe('WorkdirContextPool', function() {
assert.equal(pool.size(), 2);
+ assert.isFalse(pool.getContext(dir0).isPresent());
+ assert.isTrue(pool.getContext(dir1).isPresent());
+ assert.isTrue(pool.getContext(dir2).isPresent());
+
assert.isTrue(context0.isDestroyed());
assert.isFalse(context1.isDestroyed());
- assert.isDefined(pool.getContext(dir2));
+ });
+ });
+
+ describe('getContext', function() {
+ let dir;
+
+ beforeEach(async function() {
+ dir = await cloneRepository('three-files');
+ pool.add(dir);
+ });
+
+ it('returns a context by directory', async function() {
+ const context = pool.getContext(dir);
+ assert.isTrue(context.isPresent());
+
+ const repo = await context.getRepositoryPromise();
+ assert.strictEqual(dir, repo.getWorkingDirectoryPath());
+ });
+
+ it('returns a null context when missing', function() {
+ const context = pool.getContext('/nope');
+ assert.isFalse(context.isPresent());
});
});
});
From 785386acb6c197f6b50e1b5ab288d09a85c7f119 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 10:34:55 -0400
Subject: [PATCH 012/161] Pass a prompt callback to created contexts
---
lib/models/workdir-context.js | 4 ++++
test/models/workdir-context.test.js | 27 ++++++++++++++++++++++++++-
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 8ba510c41f..94aa6c0761 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -92,6 +92,10 @@ export default class WorkdirContext {
repo = this.getRepository();
}
+ if (this.options.promptCallback) {
+ repo.setPromptCallback(this.options.promptCallback);
+ }
+
const changeObserver = process.platform === 'linux'
? new WorkspaceChangeObserver(this.options.window, this.options.workspace, repo)
: new FileSystemChangeObserver(repo);
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 9586266427..f51d34e8a8 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -7,10 +7,22 @@ import Repository from '../../lib/models/repository';
describe('WorkdirContext', function() {
let context, workingDirectory, subs;
+ let mockWindow, mockWorkspace, mockPromptCallback;
beforeEach(async function() {
workingDirectory = await cloneRepository('three-files');
subs = new CompositeDisposable();
+
+ mockWindow = {
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ };
+
+ mockWorkspace = {
+ observeTextEditors: sinon.spy(),
+ };
+
+ mockPromptCallback = query => 'reply';
});
afterEach(async function() {
@@ -20,7 +32,11 @@ describe('WorkdirContext', function() {
describe('without an initial Repository', function() {
beforeEach(function() {
- context = new WorkdirContext(workingDirectory);
+ context = new WorkdirContext(workingDirectory, {
+ window: mockWindow,
+ workspace: mockWorkspace,
+ promptCallback: mockPromptCallback,
+ });
});
it('returns null for each synchronous model', function() {
@@ -60,6 +76,15 @@ describe('WorkdirContext', function() {
assert.strictEqual(observer, await observerPromise);
});
+ it('configures the repository with a prompt callback', async function() {
+ await context.create();
+
+ const repo = context.getRepository();
+ for (const strategy of repo.git.getImplementers()) {
+ assert.strictEqual(strategy.prompt, mockPromptCallback);
+ }
+ });
+
it('refreshes the repository on any filesystem change', async function() {
await context.create();
From f71a932adf2f5b2cd702335957087d629535931d Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 11:00:01 -0400
Subject: [PATCH 013/161] Restore saved resolution progress state
---
lib/models/workdir-context-pool.js | 14 +++++++--
test/models/workdir-context-pool.test.js | 38 +++++++++++++++++++-----
2 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index 204361b305..d42db2b130 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -8,9 +8,17 @@ import WorkdirContext, {nullWorkdirContext} from './workdir-context';
export default class WorkdirContextPool {
/**
- * Options will be passed to each `WorkdirContext` as it is created.
+ * Recognized options:
+ *
+ * - `options.resolutionProgressByPath`: saved resolution progress state associated with a previously opened
+ * working directory.
+ *
+ * All other options will be passed to each `WorkdirContext` as it is created.
*/
constructor(options = {}) {
+ this.resolutionProgressByPath = options.resolutionProgressByPath || {};
+ delete options.resolutionProgressByPath;
+
this.options = options;
this.contexts = new Map();
@@ -32,7 +40,9 @@ export default class WorkdirContextPool {
return;
}
- const context = new WorkdirContext(directory, {...this.options, ...options});
+ const savedResolutionProgress = this.resolutionProgressByPath[directory] || {};
+
+ const context = new WorkdirContext(directory, {savedResolutionProgress, ...this.options, ...options});
this.contexts.set(directory, context);
context.create();
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index dcdda06224..a3effda0bd 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -4,21 +4,28 @@ import Repository from '../../lib/models/repository';
import WorkdirContextPool from '../../lib/models/workdir-context-pool';
describe('WorkdirContextPool', function() {
- let atomEnv, workspace, pool;
+ let pool;
+ let mockWindow, mockWorkspace, mockResolutionProgressState;
beforeEach(function() {
- atomEnv = global.buildAtomEnvironment();
- workspace = atomEnv.workspace;
+ mockWindow = {
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ };
+
+ mockWorkspace = {
+ observeTextEditors: sinon.spy(),
+ };
+
+ mockResolutionProgressState = {};
pool = new WorkdirContextPool({
- window, workspace,
+ window: mockWindow,
+ workspace: mockWorkspace,
+ resolutionProgressByPath: mockResolutionProgressState,
});
});
- afterEach(function() {
- atomEnv.destroy();
- });
-
describe('add', function() {
let workingDirectory;
@@ -64,6 +71,19 @@ describe('WorkdirContextPool', function() {
const repo = await context.getRepositoryPromise();
assert.isNotNull(repo);
});
+
+ it('passes appropriate serialized state to the resolution progress', async function() {
+ mockResolutionProgressState[workingDirectory] = {
+ revision: '66d11860af6d28eb38349ef83de475597cb0e8b4',
+ paths: {'a.txt': 12},
+ };
+
+ pool.add(workingDirectory);
+
+ const resolutionProgress = await pool.getContext(workingDirectory).getResolutionProgressPromise();
+ assert.isNotNull(resolutionProgress);
+ assert.equal(resolutionProgress.getRemaining('a.txt'), 12);
+ });
});
describe('remove', function() {
@@ -129,6 +149,8 @@ describe('WorkdirContextPool', function() {
assert.isTrue(context0.isDestroyed());
assert.isFalse(context1.isDestroyed());
});
+
+ it('passes appropriate serialized state to any created resolution progresses');
});
describe('getContext', function() {
From fe59ecdcb0334499cf387c9140ae12ce5f4810fb Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 11:09:58 -0400
Subject: [PATCH 014/161] Change observers get a synchronous Repository
---
lib/models/file-system-change-observer.js | 13 +++++-----
lib/models/workspace-change-observer.js | 30 +++++++++++------------
2 files changed, 21 insertions(+), 22 deletions(-)
diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js
index 1187dffb53..e6922e5396 100644
--- a/lib/models/file-system-change-observer.js
+++ b/lib/models/file-system-change-observer.js
@@ -6,9 +6,9 @@ import path from 'path';
import EventLogger from './event-logger';
export default class FileSystemChangeObserver {
- constructor(repositoryPromise) {
+ constructor(repository) {
this.emitter = new Emitter();
- this.repositoryPromise = repositoryPromise;
+ this.repository = repository;
this.logger = new EventLogger('FileSystemChangeObserver');
}
@@ -37,10 +37,9 @@ export default class FileSystemChangeObserver {
}
async watchRepository() {
- const repository = await this.getRepository();
- if (repository) {
+ if (this.repository) {
this.currentFileWatcher = await nsfw(
- repository.getWorkingDirectoryPath(),
+ this.repository.getWorkingDirectoryPath(),
events => {
const isNonGitFile = event => !event.directory.split(path.sep).includes('.git') && event.file !== '.git';
const isWatchedGitFile = event => {
@@ -61,7 +60,7 @@ export default class FileSystemChangeObserver {
{
debounceMS: 100,
errorCallback: errors => {
- const workingDirectory = repository.getWorkingDirectoryPath();
+ const workingDirectory = this.repository.getWorkingDirectoryPath();
// eslint-disable-next-line no-console
console.warn(`Error in FileSystemChangeObserver in ${workingDirectory}:`, errors);
this.stopCurrentFileWatcher();
@@ -69,7 +68,7 @@ export default class FileSystemChangeObserver {
},
);
await this.currentFileWatcher.start();
- this.logger.showStarted(repository.getWorkingDirectoryPath());
+ this.logger.showStarted(this.repository.getWorkingDirectoryPath());
}
}
diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js
index 18755edfbd..0f99f70f2c 100644
--- a/lib/models/workspace-change-observer.js
+++ b/lib/models/workspace-change-observer.js
@@ -6,9 +6,9 @@ import {autobind} from 'core-decorators';
import EventLogger from './event-logger';
export default class WorkspaceChangeObserver {
- constructor(window, workspace, repositoryPromise) {
+ constructor(window, workspace, repository) {
this.window = window;
- this.repositoryPromise = repositoryPromise;
+ this.repository = repository;
this.workspace = workspace;
this.observedBuffers = new WeakSet();
this.emitter = new Emitter();
@@ -17,8 +17,8 @@ export default class WorkspaceChangeObserver {
async start() {
this.started = true;
- const focusHandler = async event => {
- if (await this.repositoryPromise) {
+ const focusHandler = event => {
+ if (this.repository) {
this.logger.showEvents([event]);
this.emitter.emit('did-change');
}
@@ -50,12 +50,12 @@ export default class WorkspaceChangeObserver {
}
getRepository() {
- return this.repositoryPromise;
+ return this.repository;
}
async watchActiveRepositoryGitDirectory() {
- const repository = await this.getRepository();
- const gitDirectoryPath = await repository.getGitDirectoryPath();
+ const repository = this.getRepository();
+ const gitDirectoryPath = repository.getGitDirectoryPath();
if (repository) {
this.currentFileWatcher = await nsfw(
gitDirectoryPath,
@@ -96,8 +96,8 @@ export default class WorkspaceChangeObserver {
}
}
- async activeRepositoryContainsPath(path) {
- const repository = await this.getRepository();
+ activeRepositoryContainsPath(path) {
+ const repository = this.getRepository();
if (repository) {
return path.indexOf(repository.getWorkingDirectoryPath()) !== -1;
} else {
@@ -111,8 +111,8 @@ export default class WorkspaceChangeObserver {
if (!this.observedBuffers.has(buffer)) {
this.observedBuffers.add(buffer);
const disposables = new CompositeDisposable(
- buffer.onDidSave(async () => {
- if (await this.activeRepositoryContainsPath(buffer.getPath())) {
+ buffer.onDidSave(() => {
+ if (this.activeRepositoryContainsPath(buffer.getPath())) {
this.logger.showEvents([{
handler: 'onDidSave',
path: buffer.getPath(),
@@ -120,8 +120,8 @@ export default class WorkspaceChangeObserver {
this.emitter.emit('did-change');
}
}),
- buffer.onDidReload(async () => {
- if (await this.activeRepositoryContainsPath(buffer.getPath())) {
+ buffer.onDidReload(() => {
+ if (this.activeRepositoryContainsPath(buffer.getPath())) {
this.logger.showEvents([{
handler: 'onDidReload',
path: buffer.getPath(),
@@ -129,8 +129,8 @@ export default class WorkspaceChangeObserver {
this.emitter.emit('did-change');
}
}),
- buffer.onDidDestroy(async () => {
- if (await this.activeRepositoryContainsPath(buffer.getPath())) {
+ buffer.onDidDestroy(() => {
+ if (this.activeRepositoryContainsPath(buffer.getPath())) {
this.logger.showEvents([{
handler: 'onDidDestroy',
path: buffer.getPath(),
From 696585f51619ca5502b51000d60a52d4279758f0 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 14:23:22 -0400
Subject: [PATCH 015/161] Missed one
---
lib/models/file-system-change-observer.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js
index e6922e5396..c18046ce56 100644
--- a/lib/models/file-system-change-observer.js
+++ b/lib/models/file-system-change-observer.js
@@ -33,7 +33,7 @@ export default class FileSystemChangeObserver {
}
getRepository() {
- return this.repositoryPromise;
+ return this.repository;
}
async watchRepository() {
From 27401318a728315a41f34a00df8601c108f515ca Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 14:24:01 -0400
Subject: [PATCH 016/161] Cache working directory lookups
---
lib/models/workdir-cache.js | 83 +++++++++++++++++++++++++++++++
test/models/workdir-cache.test.js | 76 ++++++++++++++++++++++++++++
2 files changed, 159 insertions(+)
create mode 100644 lib/models/workdir-cache.js
create mode 100644 test/models/workdir-cache.test.js
diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js
new file mode 100644
index 0000000000..37e924ef67
--- /dev/null
+++ b/lib/models/workdir-cache.js
@@ -0,0 +1,83 @@
+import path from 'path';
+import fs from 'fs';
+
+/**
+ * Locate the nearest git working directory above a given starting point, caching results.
+ */
+export default class WorkdirCache {
+ constructor(maxSize = 1000) {
+ this.maxSize = maxSize;
+ this.known = new Map();
+ }
+
+ async find(startDir) {
+ try {
+ const resolvedDir = await this.resolvePath(startDir);
+ const cached = this.known.get(resolvedDir);
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ const workDir = await this.walkToRoot(resolvedDir);
+
+ if (this.known.size >= this.maxSize) {
+ this.known.clear();
+ }
+ this.known.set(resolvedDir, workDir);
+ return workDir;
+ } catch (e) {
+ if (e.code === 'ENOENT') {
+ return null;
+ }
+
+ throw e;
+ }
+ }
+
+ resolvePath(unresolvedPath) {
+ return new Promise((resolve, reject) => {
+ fs.realpath(unresolvedPath, (err, resolved) => (err ? reject(err) : resolve(resolved)));
+ });
+ }
+
+ walkToRoot(initialDir) {
+ return new Promise((resolve, reject) => {
+ let currentDir = initialDir;
+
+ const check = () => {
+ const dotGit = path.join(currentDir, '.git');
+ fs.stat(dotGit, (statError, stat) => {
+ if (statError) {
+ if (statError.code === 'ENOENT') {
+ // File not found. This is not the directory we're looking for. Continue walking.
+ return walk();
+ }
+
+ reject(statError);
+ }
+
+ if (!stat.isDirectory()) {
+ // A file called ".git". Probably not a good idea, but still not a git working directory.
+ return walk();
+ }
+
+ // .git directory found! Mission accomplished.
+ return resolve(currentDir);
+ });
+ };
+
+ const walk = () => {
+ const parentDir = path.resolve(currentDir, '..');
+ if (parentDir === currentDir) {
+ // Root directory. Traversal done, no working directory found.
+ resolve(null);
+ }
+
+ currentDir = parentDir;
+ check();
+ };
+
+ check();
+ });
+ }
+}
diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js
new file mode 100644
index 0000000000..8315f3fcaf
--- /dev/null
+++ b/test/models/workdir-cache.test.js
@@ -0,0 +1,76 @@
+import path from 'path';
+import temp from 'temp';
+
+import {cloneRepository} from '../helpers';
+
+import WorkdirCache from '../../lib/models/workdir-cache';
+
+describe('WorkdirCache', function() {
+ let cache;
+
+ beforeEach(function() {
+ cache = new WorkdirCache(5);
+ });
+
+ it('finds a workdir that is the given path', async function() {
+ const sameDir = await cloneRepository('three-files');
+ const workDir = await cache.find(sameDir);
+ assert.equal(sameDir, workDir);
+ });
+
+ it("finds a workdir that's a parent of the given path", async function() {
+ const expectedDir = await cloneRepository('three-files');
+ const givenDir = path.join(expectedDir, 'subdir-1');
+ const actualDir = await cache.find(givenDir);
+
+ assert.equal(actualDir, expectedDir);
+ });
+
+ it('returns null when a path is not in a git repository', async function() {
+ const nonWorkdirPath = temp.mkdirSync();
+ assert.isNull(await cache.find(nonWorkdirPath));
+ });
+
+ it('returns null when a path does not exist', async function() {
+ const nope = path.join(
+ __dirname,
+ 'does', 'not', 'exist', 'no', 'seriously', 'why', 'did', 'you', 'name', 'a', 'directory', 'this',
+ );
+ assert.isNull(await cache.find(nope));
+ });
+
+ it('understands a file path', async function() {
+ const expectedDir = await cloneRepository('three-files');
+ const givenFile = path.join(expectedDir, 'subdir-1', 'b.txt');
+ const actualDir = await cache.find(givenFile);
+
+ assert.equal(actualDir, expectedDir);
+ });
+
+ it('caches previously discovered results', async function() {
+ const expectedDir = await cloneRepository('three-files');
+ const givenDir = path.join(expectedDir, 'subdir-1');
+
+ // Prime the cache
+ await cache.find(givenDir);
+
+ sinon.spy(cache, 'walkToRoot');
+ const actualDir = await cache.find(givenDir);
+ assert.equal(actualDir, expectedDir);
+ assert.isFalse(cache.walkToRoot.called);
+ });
+
+ it('clears the cache when the maximum size is exceeded', async function() {
+ const dirs = await Promise.all(
+ Array(6).fill(null, 0, 6).map(() => cloneRepository('three-files')),
+ );
+
+ await Promise.all(dirs.map(dir => cache.find(dir)));
+ const expectedDir = dirs[1];
+
+ sinon.spy(cache, 'walkToRoot');
+ const actualDir = await cache.find(expectedDir);
+ assert.equal(actualDir, expectedDir);
+ assert.isTrue(cache.walkToRoot.called);
+ });
+});
From 0432866dccfa3b70305e3d83a9bd9ce7c0777a66 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 16:08:58 -0400
Subject: [PATCH 017/161] Clear a pool
---
lib/models/workdir-context-pool.js | 10 ++++++++++
lib/models/workdir-context.js | 4 ++++
test/models/workdir-context-pool.test.js | 23 +++++++++++++++++++++++
3 files changed, 37 insertions(+)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index d42db2b130..1c1a4517cd 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -65,4 +65,14 @@ export default class WorkdirContextPool {
this.remove(directory);
}
}
+
+ withResidentContexts(callback) {
+ for (const [workdir, context] of this.contexts) {
+ callback(workdir, context);
+ }
+ }
+
+ clear() {
+ this.withResidentContexts(workdir => this.remove(workdir));
+ }
}
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 94aa6c0761..f4dbf05129 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -132,6 +132,10 @@ export default class WorkdirContext {
return this.emitter.on('did-change-workdir-or-head', callback);
}
+ getWorkingDirectory() {
+ return this.directory;
+ }
+
getRepository() {
return this.repositoryHolder.getValue();
}
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index a3effda0bd..a447015c1f 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -174,4 +174,27 @@ describe('WorkdirContextPool', function() {
assert.isFalse(context.isPresent());
});
});
+
+ describe('clear', function() {
+ it('removes all resident contexts', async function() {
+ const [dir0, dir1, dir2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+
+ pool.add(dir0);
+ pool.add(dir1);
+ pool.add(dir2);
+
+ assert.equal(pool.size(), 3);
+
+ pool.clear();
+
+ assert.equal(pool.size(), 0);
+ assert.isFalse(pool.getContext(dir0).isPresent());
+ assert.isFalse(pool.getContext(dir1).isPresent());
+ assert.isFalse(pool.getContext(dir2).isPresent());
+ });
+ });
});
From 7bb3a799ad062d0f9903986f5ece1737d29d42b8 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 16:20:47 -0400
Subject: [PATCH 018/161] Missed an option in the docs
---
lib/models/workdir-context.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index f4dbf05129..0e3d28f59f 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -60,6 +60,7 @@ export default class WorkdirContext {
* - `options.savedResolutionProgress`: Serialized state used to initialize the ResolutionProgress.
* - `options.window`: Browser window global, used on Linux by the WorkspaceChangeObserver.
* - `options.workspace`: Atom's workspace singleton, used on Linux by the WorkspaceChangeObserver.
+ * - `options.promptCallback`: Callback used to collect information interactively through Atom.
*/
constructor(directory, options = {}) {
this.directory = directory;
From 1b883bd54e6fe0a33219dcdc755b96a32157843c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 7 Apr 2017 16:27:14 -0400
Subject: [PATCH 019/161] Rework GitHubPackage to use the WorkdirContext
objects
---
lib/github-package.js | 322 +++++++++++++++---------------------------
1 file changed, 116 insertions(+), 206 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 64a6f712ff..af4212578c 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -7,12 +7,14 @@ import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
import compareSets from 'compare-sets';
+import WorkdirCache from './models/workdir-cache';
+import {nullWorkdirContext} from './models/workdir-context';
+import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import FileSystemChangeObserver from './models/file-system-change-observer';
import WorkspaceChangeObserver from './models/workspace-change-observer';
import ResolutionProgress from './models/conflicts/resolution-progress';
import StyleCalculator from './models/style-calculator';
-import FilePatchController from './controllers/file-patch-controller';
import RootController from './controllers/root-controller';
import IssueishPaneItem from './atom-items/issueish-pane-item';
import Switchboard from './switchboard';
@@ -35,17 +37,18 @@ export default class GithubPackage {
this.styles = styles;
this.styleCalculator = new StyleCalculator(this.styles, this.config);
- this.modelPromisesByProjectPath = new Map();
- this.modelsByProjectPath = new Map();
this.confirm = confirm;
- this.activeModelQueue = new AsyncQueue();
- this.activeProjectPath = null;
- this.activeRepository = null;
- this.activeResolutionProgress = null;
+ this.activeContextQueue = new AsyncQueue();
+ this.activeWorkdir = nullWorkdirContext;
+ this.workdirCache = new WorkdirCache();
+ this.contextPool = new WorkdirContextPool({
+ window,
+ workspace,
+ promptCallback: query => this.controller.promptForCredentials(query),
+ });
this.subscriptions = new CompositeDisposable();
- this.savedState = {};
this.switchboard = new Switchboard();
this.setupYardstick();
@@ -78,17 +81,11 @@ export default class GithubPackage {
}
activate(state = {}) {
- this.savedState = {...defaultState, ...state};
-
- this.subscriptions = new CompositeDisposable();
- const projectPaths = this.project.getDirectories().map(dir => dir.getRealPathSync());
- if (projectPaths.length > 0) {
- this.didChangeProjectPaths(projectPaths);
- }
+ const savedState = {...defaultState, ...state};
this.subscriptions.add(
- this.project.onDidChangePaths(this.didChangeProjectPaths),
- this.workspace.onDidChangeActivePaneItem(this.didChangeActivePaneItem),
+ this.project.onDidChangePaths(this.scheduleActiveContextUpdate),
+ this.workspace.onDidChangeActivePaneItem(this.scheduleActiveContextUpdate),
this.styleCalculator.startWatching(
'github-package-styles',
['editor.fontSize', 'editor.fontFamily', 'editor.lineHeight'],
@@ -114,22 +111,26 @@ export default class GithubPackage {
this.workspace.addOpener(IssueishPaneItem.opener),
);
+ this.scheduleActiveModelUpdate(savedState);
this.rerender();
}
serialize() {
const activeRepository = this.getActiveRepository();
+ const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null;
const resolutionProgressByPath = {};
- this.modelsByProjectPath.forEach((models, projectPath) => {
- const resolutionProgress = models.resolutionProgress;
- if (!resolutionProgress.isEmpty()) {
- resolutionProgressByPath[projectPath] = resolutionProgress.serialize();
+ this.activeContext.withResidentContexts((workdir, context) => {
+ const resolutionProgress = context.getResolutionProgress();
+ if (!resolutionProgress || resolutionProgress.isEmpty()) {
+ return;
}
+
+ resolutionProgressByPath[workdir] = resolutionProgress.serialize();
});
return {
- activeRepositoryPath: activeRepository ? activeRepository.getWorkingDirectoryPath() : null,
+ activeRepositoryPath,
gitController: this.controller.serialize(),
resolutionProgressByPath,
};
@@ -168,11 +169,13 @@ export default class GithubPackage {
async deactivate() {
this.subscriptions.dispose();
- if (this.destroyedRepositorySubscription) { this.destroyedRepositorySubscription.dispose(); }
- await Promise.all([
- this.destroyModelsForPaths(Array.from(this.modelPromisesByProjectPath.keys())),
- yardstick.flush(),
- ]);
+
+ this.destroyedRepositorySubscription && this.destroyedRepositorySubscription.dispose();
+ this.changedRepositorySubscription && this.changedRepositorySubscription.dispose();
+
+ this.contextPool.clear();
+
+ await yardstick.flush();
}
consumeStatusBar(statusBar) {
@@ -188,17 +191,6 @@ export default class GithubPackage {
return IssueishPaneItem.opener(uri);
}
- @autobind
- async didChangeProjectPaths(projectPaths) {
- const previousProjectPaths = Array.from(this.modelPromisesByProjectPath.keys());
- const {added, removed} = compareSets(new Set(previousProjectPaths), new Set(projectPaths));
- await Promise.all([
- this.cacheModelsForPaths(Array.from(added)),
- this.destroyModelsForPaths(Array.from(removed)),
- ]);
- await this.scheduleActiveModelUpdate();
- }
-
@autobind
async createRepositoryForProjectPath(projectPath) {
if (!projectPath) {
@@ -217,216 +209,134 @@ export default class GithubPackage {
await this.scheduleActiveModelUpdate();
}
- @autobind
- async didChangeActivePaneItem() {
- await this.scheduleActiveModelUpdate();
- }
-
- getActiveProjectPath() {
- return this.activeProjectPath;
+ getActiveWorkdir() {
+ return this.activeContext.getWorkingDirectory();
}
getActiveRepository() {
- return this.activeRepository;
+ return this.activeContext.getRepository();
}
getActiveResolutionProgress() {
- return this.activeResolutionProgress;
+ return this.activeContext.getResolutionProgress();
}
- async scheduleActiveModelUpdate(isActivating = false) {
- await this.activeModelQueue.push(this.updateActiveModels.bind(this, isActivating), {parallel: false});
+ @autobind
+ async scheduleActiveContextUpdate(savedState = {}) {
+ await this.activeContextQueue.push(this.updateActiveModels.bind(this, savedState), {parallel: false});
}
- async updateActiveModels(isActivating = false) {
- let foundActiveModels = false;
-
- let nextActiveRepository = this.activeRepository;
- let nextActiveResolutionProgress = this.activeResolutionProgress;
- let nextActiveProjectPath = this.activeProjectPath;
-
- const setByProjectPath = async projectPath => {
- if (!projectPath) {
- return;
- }
-
- nextActiveProjectPath = projectPath;
- nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
- nextActiveResolutionProgress = await this.getResolutionProgressForWorkdirPath(nextActiveProjectPath);
-
- foundActiveModels = true;
- };
-
- const activeItem = this.workspace.getActivePaneItem();
+ /**
+ * Derive the git working directory context that should be used for the package's git operations based on the current
+ * state of the Atom workspace. In priority, this prefers:
+ *
+ * - A git working directory that contains the active pane item.
+ * - A git working directory corresponding to a single Project.
+ * - When initially activating the package, the working directory that was active when the package was last
+ * serialized.
+ * - The current context, unchanged, which may be a `NullWorkdirContext`.
+ *
+ * First updates the pool of resident contexts to match all git working directories that correspond to open
+ * projects and pane items.
+ */
+ async getNextContext(savedState) {
+ const candidates = new Set(this.project.getDirectories());
+
+ this.workspace.getPaneItems()
+ .map(pathForPaneItem)
+ .filter(itemPath => !this.project.contains(itemPath))
+ .forEach(itemPath => candidates.add(itemPath));
+
+ const workdirs = await Promise.all(
+ Array.from(candidates, candidate => this.workdirCache.find(candidate)),
+ );
- if (activeItem) {
- if (typeof activeItem.getRepository === 'function') {
- // FilePatchController or other GitHub package pane
- const repository = activeItem.getRepository();
- if (!repository.isDestroyed()) {
- nextActiveRepository = repository;
- nextActiveResolutionProgress = this.props.activeResolutionProgress;
- nextActiveProjectPath = this.props.activeProjectPath;
+ this.contextPool.set(workdirs);
- foundActiveModels = true;
- }
-
- } else if (typeof activeItem.getPath === 'function') {
- // TextEditor-like
- const projectPath = this.projectPathForItemPath(activeItem.getPath());
- await setByProjectPath(projectPath);
- }
+ const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
+ if (activeItemPath) {
+ // Prefer an active item if it has a git working directory.
+ const activeWorkingDir = await this.workdirCache.find(activeItemPath);
+ if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
}
- if (!foundActiveModels && this.project.getPaths().length === 1) {
+ if (this.project.getPaths().length === 1) {
// Single project
- await setByProjectPath(this.project.getPaths()[0]);
+ const activeWorkingDir = await this.workdirCache.find(this.project.getPaths()[0]);
+ if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
}
- if (!foundActiveModels && isActivating) {
- // Restore models from saved state
- if (this.savedState.activeRepositoryPath) {
- const projectPath = this.projectPathForItemPath(this.savedState.activeRepositoryPath);
- await setByProjectPath(projectPath);
- }
+ // Restore models from saved state. Will return a NullWorkdirContext if this path is not presently
+ // resident in the pool.
+ const savedWorkingDir = savedState.activeRepositoryPath;
+ if (savedWorkingDir) {
+ return this.contextPool.getContext(savedWorkingDir);
}
- this.setActiveModels(nextActiveProjectPath, nextActiveRepository, nextActiveResolutionProgress);
+ return this.activeContext;
}
- setActiveModels(projectPath, repository, resolutionProgress) {
- // Ensure that models are updated again when a non-null repository is destroyed
- const isChanging = this.activeProjectPath !== projectPath ||
- this.activeRepository !== repository ||
- this.activeResolutionProgress !== resolutionProgress;
+ setActiveContext(nextActiveContext) {
+ if (nextActiveContext !== this.activeContext) {
+ this.contextSubs.dispose();
+ this.contextSubs = new CompositeDisposable();
- this.activeProjectPath = projectPath;
+ this.activeContext = nextActiveContext;
- if (this.activeRepository !== repository) {
- if (this.changedRepositorySubscription) { this.changedRepositorySubscription.dispose(); }
- if (this.destroyedRepositorySubscription) { this.destroyedRepositorySubscription.dispose(); }
+ this.activeContext.getRepositoryPromise().then(repository => {
+ if (!repository || repository.isDestroyed()) {
+ return;
+ }
- if (repository) {
- this.changedRepositorySubscription = repository.onDidUpdate(() => {
+ this.contextSubs.add(repository.onDidUpdate(() => {
this.switchboard.didUpdateRepository();
- });
- this.destroyedRepositorySubscription = repository.onDidDestroy(() => {
- if (repository === this.activeRepository) {
- this.setActiveModels(null, null, null);
+ }));
+ this.contextSubs.add(repository.onDidDestroy(() => {
+ if (repository === this.getActiveRepository()) {
+ this.setActiveContext(nullWorkdirContext);
}
this.destroyedRepositorySubscription.dispose();
- });
- }
- }
+ }));
+ });
- this.activeRepository = repository;
- this.activeResolutionProgress = resolutionProgress;
+ this.activeContext.getChangeObserverPromise().then(observer => {
+ this.contextSubs.add(observer.onDidChangeWorkdirOrHead(() => {
+ this.refreshAtomGitRepository(nextActiveContext.getWorkingDirectory());
+ }));
+ });
- if (isChanging) {
this.rerender();
}
}
- refreshAtomGitRepository(repository) {
- const repoPath = repository.getWorkingDirectoryPath();
- const atomGitRepo = this.project.getRepositories().find(repo => {
- return repo && path.normalize(repo.getWorkingDirectory()) === repoPath;
- });
- return atomGitRepo ? atomGitRepo.refreshStatus() : Promise.resolve();
+ async updateActiveContext(savedState = {}) {
+ const nextActiveContext = await this.getNextContext(savedState);
+ this.setActiveContext(nextActiveContext);
}
- projectPathForItemPath(filePath) {
- if (!filePath) { return null; }
- const realFilePath = new File(filePath).getRealPathSync();
- const directory = this.project.getDirectories().find(projectDir => {
- const fixedPath = projectDir.getRealPathSync() + path.sep;
- return realFilePath.startsWith(fixedPath);
+ refreshAtomGitRepository(workdir) {
+ const atomGitRepo = this.project.getRepositories().find(repo => {
+ return repo && path.normalize(repo.getWorkingDirectory()) === workdir;
});
- return directory ? directory.getRealPathSync() : null;
- }
-
- getRepositoryForWorkdirPath(workdirPath) {
- const models = this.modelPromisesByProjectPath.get(workdirPath);
- return models ? models.repositoryPromise : Promise.resolve(null);
+ return atomGitRepo ? atomGitRepo.refreshStatus() : Promise.resolve();
}
+}
- getResolutionProgressForWorkdirPath(workdirPath) {
- const models = this.modelPromisesByProjectPath.get(workdirPath);
- return models ? models.resolutionProgressPromise : Promise.resolve(null);
+function pathForPaneItem(paneItem) {
+ if (!paneItem) {
+ return null;
}
- getChangeObserverForWorkdirPath(workdirPath) {
- const models = this.modelPromisesByProjectPath.get(workdirPath);
- return models ? models.changeObserverPromise : Promise.resolve(null);
+ // Likely GitHub package provided pane item
+ if (typeof paneItem.getWorkingDirectory === 'function') {
+ return paneItem.getWorkingDirectory();
}
- cacheModelsForPaths(addedPaths) {
-
- return Promise.all(addedPaths.map(async projectPath => {
- const repository = await Repository.open(projectPath);
- if (!repository) {
- return;
- }
-
- this.addRepository(repository);
- }));
+ // TextEditor-like
+ if (typeof paneItem.getPath === 'function') {
+ return paneItem.getPath();
}
- addRepository(repository) {
- const projectPath = repository.getWorkingDirectoryPath();
- repository.setPromptCallback(query => this.controller.promptForCredentials(query));
-
- // TODO Since we need to await the repositoryPromise to see if a promise is available or not anyway, other
- // calls that are initialized with the promise aren't actually necessary anymore.
- const repositoryPromise = Promise.resolve(repository);
-
- const changeObserver = process.platform === 'linux'
- ? new WorkspaceChangeObserver(window, this.workspace, repositoryPromise)
- : new FileSystemChangeObserver(repositoryPromise);
- this.subscriptions.add(
- changeObserver.onDidChange(() => repository.refresh()),
- changeObserver.onDidChangeWorkdirOrHead(() => this.refreshAtomGitRepository(repository)),
- );
- const changeObserverPromise = changeObserver.start();
-
- const savedResolutionProgress = this.savedState.resolutionProgressByPath[projectPath] || {};
- const resolutionProgressPromise = repository.getLastCommit()
- .then(commit => new ResolutionProgress(commit.sha, savedResolutionProgress));
-
- // Once they resolve, stash the models in modelsByProjectPath for synchronous access when they're needed.
- const models = {
- repository,
- changeObserver: null,
- resolutionProgress: null,
- };
- this.modelsByProjectPath.set(projectPath, models);
- changeObserverPromise.then(() => {
- models.changeObserver = changeObserver;
- });
- resolutionProgressPromise.then(rp => {
- models.resolutionProgress = rp;
- });
-
- this.modelPromisesByProjectPath.set(projectPath, {
- repositoryPromise,
- changeObserverPromise,
- resolutionProgressPromise,
- });
- }
-
- destroyModelsForPaths(removedPaths) {
- return Promise.all(
- removedPaths.map(async projectPath => {
- const repositoryPromise = this.getRepositoryForWorkdirPath(projectPath);
- const changeObserverPromise = this.getChangeObserverForWorkdirPath(projectPath);
- this.modelPromisesByProjectPath.delete(projectPath);
-
- const repository = await repositoryPromise;
- const changeObserver = await changeObserverPromise;
- // repository and changeObserver may be null if the associated workdir was not a Git repo
- repository && repository.destroy();
- changeObserver && await changeObserver.destroy();
- }),
- );
- }
+ // Oh well
+ return null;
}
From 49487e92e9a7c11fbff6572993e1cd2ba22c9a4a Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:51:49 -0400
Subject: [PATCH 020/161] Handle ENOTDIR when a file path is given
---
lib/models/workdir-cache.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js
index 37e924ef67..aab82f4f10 100644
--- a/lib/models/workdir-cache.js
+++ b/lib/models/workdir-cache.js
@@ -48,12 +48,12 @@ export default class WorkdirCache {
const dotGit = path.join(currentDir, '.git');
fs.stat(dotGit, (statError, stat) => {
if (statError) {
- if (statError.code === 'ENOENT') {
+ if (statError.code === 'ENOENT' || statError.code === 'ENOTDIR') {
// File not found. This is not the directory we're looking for. Continue walking.
return walk();
}
- reject(statError);
+ return reject(statError);
}
if (!stat.isDirectory()) {
From e38e7e6f7dc849bfad62aeb15bb72aa4af722b64 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:52:13 -0400
Subject: [PATCH 021/161] Resolve other models when repository is null
---
lib/models/workdir-context.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 0e3d28f59f..52f099e923 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -93,6 +93,11 @@ export default class WorkdirContext {
repo = this.getRepository();
}
+ if (!repo) {
+ this.changeObserverHolder.setValue(null);
+ this.resolutionProgressHolder.setValue(null);
+ return;
+ }
if (this.options.promptCallback) {
repo.setPromptCallback(this.options.promptCallback);
}
From 769de8bdc8f7b5d20616e291ef3eee1cf69deaff Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:52:42 -0400
Subject: [PATCH 022/161] Forward observer and repository events from
WorkdirContext
---
lib/models/workdir-context.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 52f099e923..500563a5de 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -98,14 +98,20 @@ export default class WorkdirContext {
this.resolutionProgressHolder.setValue(null);
return;
}
+
if (this.options.promptCallback) {
repo.setPromptCallback(this.options.promptCallback);
}
+ // Event forwarding from repository
+ this.subs.add(repo.onDidDestroy(() => this.emitter.emit('did-destroy-repository')));
+ this.subs.add(repo.onDidUpdate(() => this.emitter.emit('did-update-repository')));
+
const changeObserver = process.platform === 'linux'
? new WorkspaceChangeObserver(this.options.window, this.options.workspace, repo)
: new FileSystemChangeObserver(repo);
+ // Event forwarding from change observer
this.subs.add(changeObserver.onDidChange(() => repo.refresh()));
this.subs.add(changeObserver.onDidChangeWorkdirOrHead(() => this.emitter.emit('did-change-workdir-or-head')));
@@ -138,6 +144,14 @@ export default class WorkdirContext {
return this.emitter.on('did-change-workdir-or-head', callback);
}
+ onDidUpdateRepository(callback) {
+ return this.emitter.on('did-update-repository', callback);
+ }
+
+ onDidDestroyRepository(callback) {
+ return this.emitter.on('did-destroy-repository', callback);
+ }
+
getWorkingDirectory() {
return this.directory;
}
From b32a4e2d5296832e43f84adeed12b28277b425f0 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:53:24 -0400
Subject: [PATCH 023/161] Forward events from all WorkdirContexts in a pool
---
lib/models/workdir-context-pool.js | 38 +++++++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index 1c1a4517cd..1df2e58d15 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -1,5 +1,6 @@
import compareSets from 'compare-sets';
+import {Emitter, CompositeDisposable} from 'event-kit';
import WorkdirContext, {nullWorkdirContext} from './workdir-context';
/**
@@ -22,6 +23,7 @@ export default class WorkdirContextPool {
this.options = options;
this.contexts = new Map();
+ this.emitter = new Emitter();
}
size() {
@@ -45,13 +47,31 @@ export default class WorkdirContextPool {
const context = new WorkdirContext(directory, {savedResolutionProgress, ...this.options, ...options});
this.contexts.set(directory, context);
+ const disposable = new CompositeDisposable();
+ const forwardEvent = (subMethod, emitEventName) => {
+ const emit = () => this.emitter.emit(emitEventName, context);
+ disposable.add(context[subMethod](emit));
+ };
+ forwardEvent('onDidChangeWorkdirOrHead', 'did-change-workdir-or-head');
+ forwardEvent('onDidUpdateRepository', 'did-update-repository');
+ forwardEvent('onDidDestroyRepository', 'did-destroy-repository');
+ disposable.add(this.onDidRemoveContext(removed => {
+ if (removed === context) {
+ disposable.dispose();
+ }
+ }));
+
context.create();
}
remove(directory) {
const existing = this.contexts.get(directory);
this.contexts.delete(directory);
- existing && existing.destroy();
+
+ if (existing) {
+ this.emitter.emit('did-remove-context', existing);
+ existing.destroy();
+ }
}
set(directories) {
@@ -72,6 +92,22 @@ export default class WorkdirContextPool {
}
}
+ onDidRemoveContext(callback) {
+ return this.emitter.on('did-remove-context', callback);
+ }
+
+ onDidUpdateRepository(callback) {
+ return this.emitter.on('did-update-repository', callback);
+ }
+
+ onDidDestroyRepository(callback) {
+ return this.emitter.on('did-destroy-repository', callback);
+ }
+
+ onDidChangeWorkdirOrHead(callback) {
+ return this.emitter.on('did-change-workdir-or-head', callback);
+ }
+
clear() {
this.withResidentContexts(workdir => this.remove(workdir));
}
From 3de282cb0a432932601b103fe7ddbe6fda4e3985 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:53:48 -0400
Subject: [PATCH 024/161] Pass options to created WorkdirContexts on set()
---
lib/models/workdir-context-pool.js | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index 1df2e58d15..e649a26a64 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -9,17 +9,9 @@ import WorkdirContext, {nullWorkdirContext} from './workdir-context';
export default class WorkdirContextPool {
/**
- * Recognized options:
- *
- * - `options.resolutionProgressByPath`: saved resolution progress state associated with a previously opened
- * working directory.
- *
- * All other options will be passed to each `WorkdirContext` as it is created.
+ * Options will be passed to each `WorkdirContext` as it is created.
*/
constructor(options = {}) {
- this.resolutionProgressByPath = options.resolutionProgressByPath || {};
- delete options.resolutionProgressByPath;
-
this.options = options;
this.contexts = new Map();
@@ -42,7 +34,10 @@ export default class WorkdirContextPool {
return;
}
- const savedResolutionProgress = this.resolutionProgressByPath[directory] || {};
+ let savedResolutionProgress = {};
+ if (options.resolutionProgressByPath) {
+ savedResolutionProgress = options.resolutionProgressByPath[directory] || {};
+ }
const context = new WorkdirContext(directory, {savedResolutionProgress, ...this.options, ...options});
this.contexts.set(directory, context);
@@ -74,12 +69,12 @@ export default class WorkdirContextPool {
}
}
- set(directories) {
+ set(directories, options = {}) {
const previous = new Set(this.contexts.keys());
const {added, removed} = compareSets(previous, directories);
for (const directory of added) {
- this.add(directory);
+ this.add(directory, options);
}
for (const directory of removed) {
this.remove(directory);
From 8571ceb1f60e4dbc710408d899d33639b9c97a37 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:54:38 -0400
Subject: [PATCH 025/161] It's activeContext, not activeWorkdir
---
lib/github-package.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 956fdc3892..ae18903be8 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -40,7 +40,7 @@ export default class GithubPackage {
this.confirm = confirm;
this.activeContextQueue = new AsyncQueue();
- this.activeWorkdir = nullWorkdirContext;
+ this.activeContext = nullWorkdirContext;
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
From d03159f16dcf4d99e95126020a6afbb201ffa7d7 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:56:52 -0400
Subject: [PATCH 026/161] Subscribe to pool events instead of repository and
observer events
---
lib/github-package.js | 44 ++++++++++++++++---------------------------
1 file changed, 16 insertions(+), 28 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index ae18903be8..6f9c654be6 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -48,9 +48,23 @@ export default class GithubPackage {
promptCallback: query => this.controller.promptForCredentials(query),
});
- this.subscriptions = new CompositeDisposable();
-
this.switchboard = new Switchboard();
+
+ // Handle events from all resident contexts.
+ this.subscriptions = new CompositeDisposable(
+ this.contextPool.onDidChangeWorkdirOrHead(context => {
+ this.refreshAtomGitRepository(context.getWorkingDirectory());
+ }),
+ this.contextPool.onDidUpdateRepository(context => {
+ this.switchboard.didUpdateRepository(context.getRepository());
+ }),
+ this.contextPool.onDidDestroyRepository(context => {
+ if (context === this.activeContext) {
+ this.setActiveContext(nullWorkdirContext);
+ }
+ }),
+ );
+
this.setupYardstick();
}
@@ -277,33 +291,7 @@ export default class GithubPackage {
setActiveContext(nextActiveContext) {
if (nextActiveContext !== this.activeContext) {
- this.contextSubs.dispose();
- this.contextSubs = new CompositeDisposable();
-
this.activeContext = nextActiveContext;
-
- this.activeContext.getRepositoryPromise().then(repository => {
- if (!repository || repository.isDestroyed()) {
- return;
- }
-
- this.contextSubs.add(repository.onDidUpdate(() => {
- this.switchboard.didUpdateRepository();
- }));
- this.contextSubs.add(repository.onDidDestroy(() => {
- if (repository === this.getActiveRepository()) {
- this.setActiveContext(nullWorkdirContext);
- }
- this.destroyedRepositorySubscription.dispose();
- }));
- });
-
- this.activeContext.getChangeObserverPromise().then(observer => {
- this.contextSubs.add(observer.onDidChangeWorkdirOrHead(() => {
- this.refreshAtomGitRepository(nextActiveContext.getWorkingDirectory());
- }));
- });
-
this.rerender();
}
}
From 993bae3353dc1f30b2a799d343154d50ab71fc4e Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:57:48 -0400
Subject: [PATCH 027/161] Remember and use savedState
---
lib/github-package.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 6f9c654be6..0b5ec435ca 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -95,7 +95,7 @@ export default class GithubPackage {
}
activate(state = {}) {
- const savedState = {...defaultState, ...state};
+ this.savedState = {...defaultState, ...state};
this.subscriptions.add(
this.project.onDidChangePaths(this.scheduleActiveContextUpdate),
@@ -125,6 +125,7 @@ export default class GithubPackage {
this.workspace.addOpener(IssueishPaneItem.opener),
);
+ this.scheduleActiveContextUpdate(this.savedState);
this.rerender();
}
@@ -264,7 +265,7 @@ export default class GithubPackage {
Array.from(candidates, candidate => this.workdirCache.find(candidate)),
);
- this.contextPool.set(workdirs);
+ this.contextPool.set(workdirs, savedState);
const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
if (activeItemPath) {
From 6eba2ac35ce88a8868acc6126d347f8a4c4f6b9f Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:58:07 -0400
Subject: [PATCH 028/161] Enumerate contextPool, not just activeContext
---
lib/github-package.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 0b5ec435ca..58e46661c5 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -134,7 +134,7 @@ export default class GithubPackage {
const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null;
const resolutionProgressByPath = {};
- this.activeContext.withResidentContexts((workdir, context) => {
+ this.contextPool.withResidentContexts((workdir, context) => {
const resolutionProgress = context.getResolutionProgress();
if (!resolutionProgress || resolutionProgress.isEmpty()) {
return;
From 34b06ff9b9b2b74016edbd7d16464f13bd7dc146 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:58:45 -0400
Subject: [PATCH 029/161] Add newly initialized and clone repositories to the
pool
---
lib/github-package.js | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 58e46661c5..b5b1fb9de1 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -211,16 +211,18 @@ export default class GithubPackage {
throw new Error('Must specify a project path to create a repository for');
}
const repository = await Repository.init(projectPath);
- this.addRepository(repository);
- await this.scheduleActiveModelUpdate();
+ this.contextPool.add(projectPath, {repository});
+
+ await this.scheduleActiveContextUpdate();
}
@autobind
async cloneRepositoryForProjectPath(remoteUrl, projectPath) {
const repository = await Repository.clone(remoteUrl, projectPath);
- this.addRepository(repository);
- this.project.addPath(projectPath);
- await this.scheduleActiveModelUpdate();
+ this.contextPool.add(projectPath, {repository});
+ this.projects.addPath(projectPath);
+
+ await this.scheduleActiveContextUpdate();
}
getActiveWorkdir() {
From 8694514698d169aa811a944c40f6e781cae930b4 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:59:00 -0400
Subject: [PATCH 030/161] Right, I renamed that
---
lib/github-package.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index b5b1fb9de1..dad98466bf 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -239,7 +239,7 @@ export default class GithubPackage {
@autobind
async scheduleActiveContextUpdate(savedState = {}) {
- await this.activeContextQueue.push(this.updateActiveModels.bind(this, savedState), {parallel: false});
+ await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false});
}
/**
From 6b04880a76c739e06e0ccccf7f94a3383e090821 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 08:59:12 -0400
Subject: [PATCH 031/161] Getter for cleaner testing
---
lib/github-package.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/github-package.js b/lib/github-package.js
index dad98466bf..ee81445aab 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -237,6 +237,10 @@ export default class GithubPackage {
return this.activeContext.getResolutionProgress();
}
+ getContextPool() {
+ return this.contextPool;
+ }
+
@autobind
async scheduleActiveContextUpdate(savedState = {}) {
await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false});
From e91ce2018801cbd8c4bfb988835bc24f2dd9b459 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:00:36 -0400
Subject: [PATCH 032/161] Iron out getNextContext
---
lib/github-package.js | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index ee81445aab..805a7dfe83 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -260,12 +260,12 @@ export default class GithubPackage {
* projects and pane items.
*/
async getNextContext(savedState) {
- const candidates = new Set(this.project.getDirectories());
+ const candidates = new Set(this.project.getPaths());
- this.workspace.getPaneItems()
- .map(pathForPaneItem)
- .filter(itemPath => !this.project.contains(itemPath))
- .forEach(itemPath => candidates.add(itemPath));
+ const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
+ if (activeItemPath) {
+ candidates.add(activeItemPath);
+ }
const workdirs = await Promise.all(
Array.from(candidates, candidate => this.workdirCache.find(candidate)),
@@ -273,17 +273,16 @@ export default class GithubPackage {
this.contextPool.set(workdirs, savedState);
- const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
if (activeItemPath) {
- // Prefer an active item if it has a git working directory.
+ // Prefer an active item
const activeWorkingDir = await this.workdirCache.find(activeItemPath);
- if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
+ return this.contextPool.getContext(activeWorkingDir);
}
if (this.project.getPaths().length === 1) {
// Single project
const activeWorkingDir = await this.workdirCache.find(this.project.getPaths()[0]);
- if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
+ return this.contextPool.getContext(activeWorkingDir);
}
// Restore models from saved state. Will return a NullWorkdirContext if this path is not presently
From 6ba316eac17490d3b8ad81c3ec6d56c390f3138b Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:00:44 -0400
Subject: [PATCH 033/161] :fire: unused imports
---
lib/github-package.js | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 805a7dfe83..2f44403f68 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -1,19 +1,15 @@
-import {CompositeDisposable, Disposable, File} from 'atom';
+import {CompositeDisposable, Disposable} from 'atom';
import path from 'path';
import React from 'react';
import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
-import compareSets from 'compare-sets';
import WorkdirCache from './models/workdir-cache';
import {nullWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
-import FileSystemChangeObserver from './models/file-system-change-observer';
-import WorkspaceChangeObserver from './models/workspace-change-observer';
-import ResolutionProgress from './models/conflicts/resolution-progress';
import StyleCalculator from './models/style-calculator';
import RootController from './controllers/root-controller';
import IssueishPaneItem from './atom-items/issueish-pane-item';
From c318f1970ef8a0e541cef78a958bc3f8bee90516 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:00:58 -0400
Subject: [PATCH 034/161] HULK COMMIT GIANT TEST OVERHAUL
---
test/github-package.test.js | 499 ++++++++++++++++++------------------
1 file changed, 254 insertions(+), 245 deletions(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 0d8db5662d..979aad6655 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -1,16 +1,15 @@
-import {Directory} from 'atom';
-
import fs from 'fs';
import path from 'path';
import temp from 'temp';
import until from 'test-until';
import {cloneRepository} from './helpers';
+import {writeFile} from '../lib/helpers';
import GithubPackage from '../lib/github-package';
describe('GithubPackage', function() {
let atomEnv, workspace, project, commandRegistry, notificationManager, config, confirm, tooltips, styles;
- let githubPackage;
+ let githubPackage, contextPool;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
@@ -25,6 +24,8 @@ describe('GithubPackage', function() {
githubPackage = new GithubPackage(
workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm,
);
+
+ contextPool = githubPackage.getContextPool();
});
afterEach(async function() {
@@ -32,47 +33,29 @@ describe('GithubPackage', function() {
atomEnv.destroy();
});
- // Create a Promise that will resolve successfully when all models for all expected working directories have been
- // initialized.
- function untilModelsReady(workdirs, models = ['respository', 'changeObserver', 'resolutionProgress'], pkg = githubPackage) {
- const modelPromiseGetters = models.map(modelName => {
- return `get${modelName[0].toUpperCase() + modelName.slice(1)}ForWorkdirPath`;
- });
-
- const modelReadyForWorkdir = (getter, workdir) => new Promise(resolve => {
- const attempt = () => {
- pkg[getter](workdir).then(result => {
- if (result === null) {
- setTimeout(attempt, 100);
- } else {
- resolve();
- }
- });
- };
- attempt();
+ function untilWorkdirs(...workdirs) {
+ return until(`working directories ${workdirs.join(', ')} are loaded`, () => {
+ return workdirs.every(workdir => contextPool.getContext(workdir).isPresent());
});
+ }
- const readyPromises = [];
- for (let i = 0; i < workdirs.length; i++) {
- const workdir = workdirs[i];
- for (let j = 0; j < modelPromiseGetters.length; j++) {
- const getter = modelPromiseGetters[j];
- readyPromises.push(modelReadyForWorkdir(getter, workdir));
- }
- }
- return Promise.all(readyPromises);
+ function untilRepositories(...workdirs) {
+ return untilWorkdirs(...workdirs).then(() => Promise.all(
+ workdirs.map(workdir => contextPool.getContext(workdir).getRepositoryPromise()),
+ ));
}
describe('activate()', function() {
it('uses models from preexisting projects', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
- await assert.async.isNotNull(await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
- await assert.async.isNotNull(await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
+ await untilRepositories(workdirPath1, workdirPath2);
assert.isNull(githubPackage.getActiveRepository());
});
@@ -83,31 +66,29 @@ describe('GithubPackage', function() {
await githubPackage.activate();
await assert.async.isNotNull(githubPackage.getActiveRepository());
- assert.equal(
- await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
- workdirPath,
- );
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath);
});
it('uses an active model from a preexisting active pane item', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
project.setPaths([workdirPath1, workdirPath2]);
await workspace.open(path.join(workdirPath2, 'a.txt'));
await githubPackage.activate();
await assert.async.isNotNull(githubPackage.getActiveRepository());
- assert.equal(
- await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
- workdirPath2,
- );
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
it('uses an active model from serialized state', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
- const workdirPath3 = await cloneRepository('three-files');
+ const [workdirPath1, workdirPath2, workdirPath3] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
await githubPackage.activate({
@@ -115,15 +96,14 @@ describe('GithubPackage', function() {
});
await assert.async.isNotNull(githubPackage.getActiveRepository());
- assert.equal(
- await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
- workdirPath2,
- );
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
it('prefers the active model from an active pane item to serialized state', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
project.setPaths([workdirPath1, workdirPath2]);
await workspace.open(path.join(workdirPath2, 'b.txt'));
@@ -132,15 +112,14 @@ describe('GithubPackage', function() {
});
await assert.async.isNotNull(githubPackage.getActiveRepository());
- assert.equal(
- await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
- workdirPath2,
- );
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
it('prefers the active model from a single project to serialized state', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
project.setPaths([workdirPath1]);
await githubPackage.activate({
@@ -148,63 +127,84 @@ describe('GithubPackage', function() {
});
await assert.async.isNotNull(githubPackage.getActiveRepository());
- assert.equal(
- await githubPackage.getActiveRepository().getWorkingDirectoryPath(),
- workdirPath1,
- );
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath1);
});
- });
- describe('changing the project paths', function() {
- it('updates the active repository and project path', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
+ it('restores the active resolution progress', async function() {
+ // Repository with a merge conflict, repository without a merge conflict, path without a repository
+ const workdirMergeConflict = await cloneRepository('merge-conflict');
+ const workdirNoConflict = await cloneRepository('three-files');
const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
- project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
- fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'change 1', 'utf8');
+
+ project.setPaths([workdirMergeConflict, workdirNoConflict, nonRepositoryPath]);
await githubPackage.activate();
- sinon.spy(githubPackage, 'rerender');
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- const repository1 = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
- await assert.async.strictEqual(githubPackage.getActiveRepository(), repository1);
- await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath1);
- await assert.async.equal(githubPackage.rerender.callCount, 1);
+ // Open a file in the merge conflict repository.
+ await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-ours.txt'));
+ await githubPackage.scheduleActiveContextUpdate();
- // Remove repository for open file
- project.setPaths([workdirPath2, nonRepositoryPath]);
- await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath1);
- await assert.async.isNull(githubPackage.getActiveRepository());
- await assert.async.equal(githubPackage.rerender.callCount, 2);
+ const resolutionMergeConflict = await contextPool.getContext(workdirMergeConflict).getResolutionProgressPromise();
+ await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
- await workspace.open(path.join(workdirPath2, 'b.txt'));
- const repository2 = await githubPackage.getRepositoryForWorkdirPath(workdirPath2);
- await assert.async.strictEqual(githubPackage.getActiveRepository(), repository2);
- await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath2);
- await assert.async.equal(githubPackage.rerender.callCount, 3);
+ // Record some resolution progress to recall later
+ resolutionMergeConflict.reportMarkerCount('modified-on-both-ours.txt', 3);
+
+ // Open a file in the non-merge conflict repository.
+ await workspace.open(path.join(workdirNoConflict, 'b.txt'));
+ await githubPackage.scheduleActiveContextUpdate();
+ const resolutionNoConflict = await contextPool.getContext(workdirNoConflict).getResolutionProgressPromise();
+ assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionNoConflict);
+ assert.isTrue(githubPackage.getActiveResolutionProgress().isEmpty());
+
+ // Open a file in the workdir with no repository.
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
- await assert.async.isNull(githubPackage.getActiveRepository());
- await assert.async.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
- await assert.async.equal(githubPackage.rerender.callCount, 4);
+ await githubPackage.scheduleActiveContextUpdate();
+
+ assert.isNull(githubPackage.getActiveResolutionProgress());
+
+ // Re-open a file in the merge conflict repository.
+ await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-theirs.txt'));
+ await githubPackage.scheduleActiveContextUpdate();
+
+ assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
+ assert.isFalse(githubPackage.getActiveResolutionProgress().isEmpty());
+ assert.equal(githubPackage.getActiveResolutionProgress().getRemaining('modified-on-both-ours.txt'), 3);
});
+ });
- it('destroys all the repositories associated with the removed project folders', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
- const workdirPath3 = await cloneRepository('three-files');
- project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
+ describe('when the project paths change', function() {
+ it('adds new workdirs to the pool', async function() {
+ const [workdirPath1, workdirPath2, workdirPath3] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+
+ project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
+ await untilWorkdirs(workdirPath1, workdirPath2);
+
+ project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
+ await untilWorkdirs(workdirPath1, workdirPath2, workdirPath3);
+ });
- await untilModelsReady([workdirPath1, workdirPath2, workdirPath3], ['repository']);
+ it('destroys contexts associated with the removed project folders', async function() {
+ const [workdirPath1, workdirPath2, workdirPath3] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
+ await githubPackage.activate();
+ await untilRepositories(workdirPath1, workdirPath2, workdirPath3);
- const repository1 = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
- const repository2 = await githubPackage.getRepositoryForWorkdirPath(workdirPath2);
- const repository3 = await githubPackage.getRepositoryForWorkdirPath(workdirPath3);
- assert.isOk(repository1);
- assert.isOk(repository2);
- assert.isOk(repository3);
+ const [repository1, repository2, repository3] = await Promise.all(
+ [workdirPath1, workdirPath2, workdirPath3].map(workdir => {
+ return contextPool.getContext(workdir).getRepositoryPromise();
+ }),
+ );
sinon.stub(repository1, 'destroy');
sinon.stub(repository2, 'destroy');
@@ -213,139 +213,173 @@ describe('GithubPackage', function() {
project.removePath(workdirPath1);
project.removePath(workdirPath3);
- await until(() => repository1.destroy.callCount === 1, 'repository1 is destroyed');
- await until(() => repository3.destroy.callCount === 1, 'repository3 is destroyed');
- assert.equal(repository2.destroy.callCount, 0);
- assert.notEqual(await githubPackage.getRepositoryForWorkdirPath(repository1.getWorkingDirectoryPath()), repository1);
- assert.notEqual(await githubPackage.getRepositoryForWorkdirPath(repository3.getWorkingDirectoryPath()), repository3);
- assert.equal(await githubPackage.getRepositoryForWorkdirPath(repository2.getWorkingDirectoryPath()), repository2);
+ await assert.async.equal(repository1.destroy.callCount, 1);
+ await assert.async.equal(repository3.destroy.callCount, 1);
+ assert.isFalse(repository2.destroy.called);
+
+ assert.isFalse(contextPool.getContext(workdirPath1).isPresent());
+ assert.isFalse(contextPool.getContext(workdirPath3).isPresent());
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
});
});
- describe('didChangeActivePaneItem()', function() {
- it('updates the active repository', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
+ describe('when the active pane item changes', function() {
+ it('becomes the active context', async function() {
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
project.setPaths([workdirPath1, workdirPath2]);
- fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'change 1', 'utf8');
- fs.writeFileSync(path.join(workdirPath2, 'b.txt'), 'change 2', 'utf8');
+ await githubPackage.activate();
+ await untilRepositories(workdirPath1, workdirPath2);
+ const repository2 = contextPool.getContext(workdirPath2).getRepository();
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- githubPackage.didChangeActivePaneItem();
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
+ assert.isNull(githubPackage.getActiveRepository());
await workspace.open(path.join(workdirPath2, 'b.txt'));
- githubPackage.didChangeActivePaneItem();
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
+
+ await assert.async.strictEqual(githubPackage.getActiveRepository(), repository2);
});
- });
- describe('updateActiveModels()', function() {
- it('updates the active repository based on the active item, setting it to null when the active item is not in a project repository', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
- const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
- fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
- project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
+ it('adds a new context if not in a project', async function() {
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1]);
await githubPackage.activate();
+ await untilRepositories(workdirPath1);
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- await workspace.open(path.join(workdirPath2, 'b.txt'));
+ await workspace.open(path.join(workdirPath2, 'c.txt'));
- await githubPackage.scheduleActiveModelUpdate();
- assert.isNotNull(githubPackage.getActiveRepository());
- assert.equal(githubPackage.getActiveProjectPath(), workdirPath2);
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
+ await untilRepositories(workdirPath1, workdirPath2);
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
+ });
- await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
- await githubPackage.scheduleActiveModelUpdate();
- assert.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
- assert.isNull(githubPackage.getActiveRepository());
+ it('removes a context if not in a project', async function() {
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1]);
+ await githubPackage.activate();
+ await workspace.open(path.join(workdirPath2, 'c.txt'));
+ await untilRepositories(workdirPath1, workdirPath2);
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- await githubPackage.scheduleActiveModelUpdate();
- assert.equal(githubPackage.getActiveProjectPath(), workdirPath1);
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
+ workspace.getActivePane().destroyActiveItem();
- workspace.getActivePane().activateItem({}); // such as when find & replace results pane is focused
- await githubPackage.scheduleActiveModelUpdate();
- assert.equal(githubPackage.getActiveProjectPath(), workdirPath1);
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
+ await assert.async.isFalse(contextPool.getContext(workdirPath2).isPresent());
+ });
+ });
- await workspace.open(path.join(workdirPath2, 'b.txt'));
- await githubPackage.scheduleActiveModelUpdate();
- assert.equal(githubPackage.getActiveProjectPath(), workdirPath2);
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
+ describe('scheduleActiveContextUpdate()', function() {
+ beforeEach(function() {
+ // Necessary since we skip activate()
+ githubPackage.savedState = {};
+ });
- project.removePath(workdirPath2);
- await githubPackage.scheduleActiveModelUpdate();
- assert.isNull(githubPackage.getActiveProjectPath());
- assert.isNull(githubPackage.getActiveRepository());
+ it('prefers the context of the active pane item', async function() {
+ const [workdirPath1, workdirPath2, workdirPath3] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1]);
+ await workspace.open(path.join(workdirPath2, 'a.txt'));
- project.removePath(workdirPath1);
- await githubPackage.scheduleActiveModelUpdate();
- assert.isNull(githubPackage.getActiveProjectPath());
- assert.isNull(githubPackage.getActiveRepository());
+ await githubPackage.scheduleActiveContextUpdate({
+ activeRepositoryPath: workdirPath3,
+ });
+
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
+ });
+
+ it('uses an empty context when the active item is not in a git repository', async function() {
+ const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
+ const workdir = await cloneRepository('three-files');
+ project.setPaths([nonRepositoryPath, workdir]);
+ await writeFile(path.join(nonRepositoryPath, 'a.txt'), 'stuff');
+ await workspace.open(path.join(nonRepositoryPath, 'a.txt'));
+
+ await githubPackage.scheduleActiveContextUpdate();
- await workspace.open(path.join(workdirPath1, 'a.txt'));
- await githubPackage.scheduleActiveModelUpdate();
- assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
});
- it('defaults to a single repository even without an active pane item', async function() {
- const workdirPath = await cloneRepository('three-files');
- project.setPaths([workdirPath]);
- await githubPackage.activate();
+ it('uses the context of a single open project', async function() {
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1]);
- await githubPackage.scheduleActiveModelUpdate();
+ await githubPackage.scheduleActiveContextUpdate({
+ activeRepositoryPath: workdirPath2,
+ });
- const repository = await githubPackage.getRepositoryForWorkdirPath(workdirPath);
- await assert.async.strictEqual(githubPackage.getActiveRepository(), repository);
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath1);
});
- it('updates the active resolution progress', async function() {
- // Repository with a merge conflict, repository without a merge conflict, path without a repository
- const workdirMergeConflict = await cloneRepository('merge-conflict');
- const workdirNoConflict = await cloneRepository('three-files');
+ it('uses a null context with a single open project without a git workdir', async function() {
const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
- fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
+ project.setPaths([nonRepositoryPath]);
- project.setPaths([workdirMergeConflict, workdirNoConflict, nonRepositoryPath]);
- await githubPackage.activate();
- await untilModelsReady([workdirMergeConflict, workdirNoConflict], ['resolutionProgress']);
+ await githubPackage.scheduleActiveContextUpdate();
- // Open a file in the merge conflict repository.
- await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-ours.txt'));
- await githubPackage.scheduleActiveModelUpdate();
+ assert.isNull(githubPackage.getActiveRepository());
+ });
- const resolutionMergeConflict = await githubPackage.getResolutionProgressForWorkdirPath(workdirMergeConflict);
- await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
+ it('activates a saved context state', async function() {
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1, workdirPath2]);
- // Record some resolution progress to recall later
- resolutionMergeConflict.reportMarkerCount('modified-on-both-ours.txt', 3);
+ await githubPackage.scheduleActiveContextUpdate({
+ activeRepositoryPath: workdirPath2,
+ });
- // Open a file in the non-merge conflict repository.
- await workspace.open(path.join(workdirNoConflict, 'b.txt'));
- await githubPackage.scheduleActiveModelUpdate();
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
+ });
- const resolutionNoConflict = await githubPackage.getResolutionProgressForWorkdirPath(workdirNoConflict);
- await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionNoConflict);
- assert.isTrue(githubPackage.getActiveResolutionProgress().isEmpty());
+ it('falls back to keeping the context the same', async function() {
+ const [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+ project.setPaths([workdirPath1, workdirPath2]);
- // Open a file in the workdir with no repository.
- await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
- await githubPackage.scheduleActiveModelUpdate();
+ contextPool.set([workdirPath1, workdirPath2]);
+ githubPackage.setActiveContext(contextPool.getContext(workdirPath1));
- await assert.isNull(githubPackage.getActiveResolutionProgress());
+ await githubPackage.scheduleActiveContextUpdate();
- // Re-open a file in the merge conflict repository.
- await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-theirs.txt'));
- await githubPackage.scheduleActiveModelUpdate();
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath1);
+ });
- await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
- assert.isFalse(githubPackage.getActiveResolutionProgress().isEmpty());
- assert.equal(githubPackage.getActiveResolutionProgress().getRemaining('modified-on-both-ours.txt'), 3);
+ it('discovers a context from an open subdirectory', async function() {
+ const workdirPath = await cloneRepository('three-files');
+ const projectPath = path.join(workdirPath, 'subdir-1');
+ project.setPaths([projectPath]);
+
+ await githubPackage.scheduleActiveContextUpdate();
+
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath);
+ });
+
+ it('reverts to an empty context if the active repository is destroyed', async function() {
+ const workdirPath = await cloneRepository('three-files');
+ project.setPaths([workdirPath]);
+
+ await githubPackage.scheduleActiveContextUpdate();
+
+ const repository = await contextPool.getContext(workdirPath).getRepositoryPromise();
+ assert.isNotNull(repository);
+
+ repository.destroy();
+
+ assert.isNull(githubPackage.getActiveRepository());
});
// Don't worry about this on Windows as it's not a common op
@@ -355,13 +389,10 @@ describe('GithubPackage', function() {
const symlinkPath = fs.realpathSync(temp.mkdirSync()) + '-symlink';
fs.symlinkSync(workdirPath, symlinkPath);
project.setPaths([symlinkPath]);
- await githubPackage.activate();
- await untilModelsReady([workdirPath], ['repository']);
-
await workspace.open(path.join(symlinkPath, 'a.txt'));
- await githubPackage.scheduleActiveModelUpdate();
- assert.isOk(githubPackage.getActiveRepository());
+ await githubPackage.scheduleActiveContextUpdate();
+ await assert.async.isOk(githubPackage.getActiveRepository());
});
}
});
@@ -371,38 +402,24 @@ describe('GithubPackage', function() {
let workdirPath2, atomGitRepository2, repository2;
beforeEach(async function() {
- workdirPath1 = await cloneRepository('three-files');
- workdirPath2 = await cloneRepository('three-files');
+ [workdirPath1, workdirPath2] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
fs.writeFileSync(path.join(workdirPath1, 'c.txt'), 'ch-ch-ch-changes', 'utf8');
fs.writeFileSync(path.join(workdirPath2, 'c.txt'), 'ch-ch-ch-changes', 'utf8');
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
-
- // Wait for all change observers to be present.
- const changeObserverForWorkdir = workdirPath => new Promise(resolve => {
- const attempt = () => {
- githubPackage.getChangeObserverForWorkdirPath(workdirPath).then(result => {
- if (result === null) {
- setTimeout(attempt, 100);
- } else {
- resolve();
- }
- });
- };
- attempt();
- });
- await Promise.all(
- [workdirPath1, workdirPath2].map(workdirPath => changeObserverForWorkdir(workdirPath)),
- );
+ await untilRepositories(workdirPath1, workdirPath2);
[atomGitRepository1, atomGitRepository2] = githubPackage.project.getRepositories();
sinon.stub(atomGitRepository1, 'refreshStatus');
sinon.stub(atomGitRepository2, 'refreshStatus');
- repository1 = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
- repository2 = await githubPackage.getRepositoryForWorkdirPath(workdirPath2);
+ repository1 = contextPool.getContext(workdirPath1).getRepository();
+ repository2 = contextPool.getContext(workdirPath2).getRepository();
sinon.stub(repository1, 'refresh');
sinon.stub(repository2, 'refresh');
});
@@ -444,7 +461,7 @@ describe('GithubPackage', function() {
});
});
- describe('#createRepositoryForProjectPath', function() {
+ describe('createRepositoryForProjectPath()', function() {
it('creates and sets a repository for the given project path', async function() {
const workdirPath1 = await cloneRepository('three-files');
const workdirPath2 = await cloneRepository('three-files');
@@ -457,36 +474,28 @@ describe('GithubPackage', function() {
await assert.async.isNull(githubPackage.getActiveRepository());
await githubPackage.createRepositoryForProjectPath(nonRepositoryPath);
assert.isOk(githubPackage.getActiveRepository());
- assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(nonRepositoryPath));
- });
- });
-
- describe('#projectPathForItemPath', function() {
- it('does not error when the path is falsy (e.g. new unsaved file)', function() {
- sinon.stub(project, 'getDirectories').returns([new Directory('path')]);
- assert.doesNotThrow(() => {
- githubPackage.projectPathForItemPath(null);
- });
- });
-
- it('returns the correct path when the item path starts with the project path but the item path is not in the project', function() {
- const dirs = [path.join('path', 'to', 'my'), path.join('path', 'to', 'my-project')].map(p => new Directory(p));
- sinon.stub(project, 'getDirectories').returns(dirs);
- assert.equal(githubPackage.projectPathForItemPath(path.join('path', 'to', 'my-project', 'file.txt')), path.join('path', 'to', 'my-project'));
+ assert.strictEqual(
+ githubPackage.getActiveRepository(),
+ await contextPool.getContext(nonRepositoryPath).getRepository(),
+ );
});
});
describe('serialized state', function() {
+ function resolutionProgressFrom(pkg, workdir) {
+ return pkg.getContextPool().getContext(workdir).getResolutionProgressPromise();
+ }
+
it('restores nonempty resolution progress', async function() {
const workdirMergeConflict = await cloneRepository('merge-conflict');
const workdirNoConflict = await cloneRepository('three-files');
project.setPaths([workdirMergeConflict, workdirNoConflict]);
await githubPackage.activate();
- await untilModelsReady([workdirMergeConflict, workdirNoConflict], ['resolutionProgress']);
+ await githubPackage.scheduleActiveContextUpdate();
// Record some state to recover later.
- const resolutionMergeConflict0 = await githubPackage.getResolutionProgressForWorkdirPath(workdirMergeConflict);
+ const resolutionMergeConflict0 = await resolutionProgressFrom(githubPackage, workdirMergeConflict);
resolutionMergeConflict0.reportMarkerCount('modified-on-both-ours.txt', 3);
const payload = githubPackage.serialize();
@@ -500,10 +509,10 @@ describe('GithubPackage', function() {
workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm,
);
await githubPackage1.activate(payload);
- await untilModelsReady([workdirMergeConflict, workdirNoConflict], ['resolutionProgress'], githubPackage1);
+ await githubPackage1.scheduleActiveContextUpdate();
- const resolutionMergeConflict1 = await githubPackage1.getResolutionProgressForWorkdirPath(workdirMergeConflict);
- const resolutionNoConflict1 = await githubPackage1.getResolutionProgressForWorkdirPath(workdirNoConflict);
+ const resolutionMergeConflict1 = await resolutionProgressFrom(githubPackage1, workdirMergeConflict);
+ const resolutionNoConflict1 = await resolutionProgressFrom(githubPackage1, workdirNoConflict);
assert.isFalse(resolutionMergeConflict1.isEmpty());
assert.equal(resolutionMergeConflict1.getRemaining('modified-on-both-ours.txt'), 3);
From bca60166054c1c409b61b01c138c9dc26bd6c4fa Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:04:39 -0400
Subject: [PATCH 035/161] Import Disposables from event-kit
---
lib/models/workdir-context.js | 2 +-
lib/switchboard.js | 2 +-
test/models/workdir-context.test.js | 2 +-
test/switchboard.test.js | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 500563a5de..a037abbc73 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -1,4 +1,4 @@
-import {Emitter, CompositeDisposable} from 'atom'; // FIXME import from event-kit
+import {Emitter, CompositeDisposable} from 'event-kit';
import Repository from './repository';
import ResolutionProgress from './conflicts/resolution-progress';
diff --git a/lib/switchboard.js b/lib/switchboard.js
index 50c4e2d27d..a00cb14c6c 100644
--- a/lib/switchboard.js
+++ b/lib/switchboard.js
@@ -1,4 +1,4 @@
-import {Emitter} from 'atom'; // FIXME import from event-kit instead
+import {Emitter} from 'event-kit';
/*
* Register callbacks and construct Promises to wait for the next occurrence of specific events that occur throughout
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index f51d34e8a8..3ae8c42223 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -1,4 +1,4 @@
-import {CompositeDisposable} from 'atom';
+import {CompositeDisposable} from 'event-kit';
import {cloneRepository} from '../helpers';
diff --git a/test/switchboard.test.js b/test/switchboard.test.js
index 1403a6634e..41e624fd6d 100644
--- a/test/switchboard.test.js
+++ b/test/switchboard.test.js
@@ -1,4 +1,4 @@
-import {CompositeDisposable} from 'atom'; // FIXME import from event-kit
+import {CompositeDisposable} from 'event-kit';
import Switchboard from '../lib/switchboard';
describe('Switchboard', function() {
From a740ed4a8a1a04250f4747f53ffbd181f4837cd7 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:14:19 -0400
Subject: [PATCH 036/161] Create NullWorkdirContexts that track working
directory
---
lib/github-package.js | 6 +++---
lib/models/workdir-context-pool.js | 4 ++--
lib/models/workdir-context.js | 8 +++-----
3 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 2b36bb234c..55cec9fd0e 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -7,7 +7,7 @@ import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
import WorkdirCache from './models/workdir-cache';
-import {nullWorkdirContext} from './models/workdir-context';
+import {NullWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import StyleCalculator from './models/style-calculator';
@@ -36,7 +36,7 @@ export default class GithubPackage {
this.confirm = confirm;
this.activeContextQueue = new AsyncQueue();
- this.activeContext = nullWorkdirContext;
+ this.activeContext = new NullWorkdirContext(null);
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
@@ -56,7 +56,7 @@ export default class GithubPackage {
}),
this.contextPool.onDidDestroyRepository(context => {
if (context === this.activeContext) {
- this.setActiveContext(nullWorkdirContext);
+ this.setActiveContext(new NullWorkdirContext(context.getWorkingDirectory()));
}
}),
);
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index e649a26a64..647537c263 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -1,7 +1,7 @@
import compareSets from 'compare-sets';
import {Emitter, CompositeDisposable} from 'event-kit';
-import WorkdirContext, {nullWorkdirContext} from './workdir-context';
+import WorkdirContext, {NullWorkdirContext} from './workdir-context';
/**
* Manage a WorkdirContext for each open directory.
@@ -26,7 +26,7 @@ export default class WorkdirContextPool {
* Access the context mapped to a known directory.
*/
getContext(directory) {
- return this.contexts.get(directory) || nullWorkdirContext;
+ return this.contexts.get(directory) || new NullWorkdirContext(directory);
}
add(directory, options = {}) {
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index a037abbc73..408a24232a 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -203,9 +203,9 @@ export default class WorkdirContext {
}
}
-class NullWorkdirContext extends WorkdirContext {
- constructor() {
- super(null, {});
+export class NullWorkdirContext extends WorkdirContext {
+ constructor(directory) {
+ super(directory, {});
this.repositoryHolder.setValue(null);
this.changeObserverHolder.setValue(null);
@@ -224,5 +224,3 @@ class NullWorkdirContext extends WorkdirContext {
return Promise.resolve();
}
}
-
-export const nullWorkdirContext = new NullWorkdirContext();
From 71dfb8b8eb46aa1f3963c2541b6b9cfe289bf3ff Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:16:26 -0400
Subject: [PATCH 037/161] Right, I changed the way resolution progress state is
passed
---
test/models/workdir-context-pool.test.js | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index a447015c1f..b4a10a6e86 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -5,7 +5,7 @@ import WorkdirContextPool from '../../lib/models/workdir-context-pool';
describe('WorkdirContextPool', function() {
let pool;
- let mockWindow, mockWorkspace, mockResolutionProgressState;
+ let mockWindow, mockWorkspace;
beforeEach(function() {
mockWindow = {
@@ -17,12 +17,9 @@ describe('WorkdirContextPool', function() {
observeTextEditors: sinon.spy(),
};
- mockResolutionProgressState = {};
-
pool = new WorkdirContextPool({
window: mockWindow,
workspace: mockWorkspace,
- resolutionProgressByPath: mockResolutionProgressState,
});
});
@@ -73,12 +70,14 @@ describe('WorkdirContextPool', function() {
});
it('passes appropriate serialized state to the resolution progress', async function() {
- mockResolutionProgressState[workingDirectory] = {
- revision: '66d11860af6d28eb38349ef83de475597cb0e8b4',
- paths: {'a.txt': 12},
+ const resolutionProgressByPath = {
+ [workingDirectory]: {
+ revision: '66d11860af6d28eb38349ef83de475597cb0e8b4',
+ paths: {'a.txt': 12},
+ },
};
- pool.add(workingDirectory);
+ pool.add(workingDirectory, {resolutionProgressByPath});
const resolutionProgress = await pool.getContext(workingDirectory).getResolutionProgressPromise();
assert.isNotNull(resolutionProgress);
From dd38b9fe72ab9487beefffb98a8e5bf9219c9434 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:29:06 -0400
Subject: [PATCH 038/161] Implement getWorkingDirectory() in
FilePatchController
---
lib/controllers/file-patch-controller.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js
index 53089207b2..ec43c867ed 100644
--- a/lib/controllers/file-patch-controller.js
+++ b/lib/controllers/file-patch-controller.js
@@ -9,6 +9,7 @@ import FilePatchView from '../views/file-patch-view';
export default class FilePatchController extends React.Component {
static propTypes = {
+ activeWorkingDirectory: React.PropTypes.string,
repository: React.PropTypes.object,
commandRegistry: React.PropTypes.object.isRequired,
filePatch: React.PropTypes.object.isRequired,
@@ -222,6 +223,13 @@ export default class FilePatchController extends React.Component {
return this.props.repository.hasDiscardHistory(this.props.filePatch.getPath());
}
+ /**
+ * Used to detect the context when this PaneItem is active
+ */
+ getWorkingDirectory() {
+ return this.props.activeWorkingDirectory;
+ }
+
destroy() {
this.emitter.emit('did-destroy');
}
From 4a7ccf014b7629e5820fd767232eec10012ade23 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:29:34 -0400
Subject: [PATCH 039/161] Rename activeProjectPath for accuracy
---
lib/controllers/root-controller.js | 5 +++--
lib/github-package.js | 9 +++++----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index b119ad6332..33858b34a1 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -43,7 +43,7 @@ export default class RootController extends React.Component {
tooltips: React.PropTypes.object.isRequired,
config: React.PropTypes.object.isRequired,
confirm: React.PropTypes.func.isRequired,
- activeProjectPath: React.PropTypes.string,
+ activeWorkingDirectory: React.PropTypes.string,
createRepositoryForProjectPath: React.PropTypes.func,
cloneRepositoryForProjectPath: React.PropTypes.func,
repository: React.PropTypes.object,
@@ -202,6 +202,7 @@ export default class RootController extends React.Component {
ref={c => { this.filePatchControllerPane = c; }}
onDidCloseItem={() => { this.setState({...nullFilePatchState}); }}>
Date: Tue, 11 Apr 2017 09:36:25 -0400
Subject: [PATCH 040/161] untilRepositories() won't wait until the active
context is set
---
test/github-package.test.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 979aad6655..cad42c4659 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -252,8 +252,7 @@ describe('GithubPackage', function() {
await workspace.open(path.join(workdirPath2, 'c.txt'));
- await untilRepositories(workdirPath1, workdirPath2);
- assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
+ await assert.async.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
it('removes a context if not in a project', async function() {
From bbb6fdcb5cfd16c1e4347e8bc4b1dcf531b5a705 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 09:41:07 -0400
Subject: [PATCH 041/161] Wait for Repository creation
---
test/github-package.test.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index cad42c4659..de8f34174f 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -472,6 +472,8 @@ describe('GithubPackage', function() {
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
await assert.async.isNull(githubPackage.getActiveRepository());
await githubPackage.createRepositoryForProjectPath(nonRepositoryPath);
+ await contextPool.getContext(nonRepositoryPath).getRepositoryPromise();
+
assert.isOk(githubPackage.getActiveRepository());
assert.strictEqual(
githubPackage.getActiveRepository(),
From 03d7471efb43cecbc7da816a18fcbda2aebe3ff1 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 10:23:09 -0400
Subject: [PATCH 042/161] Replace existing contexts within a pool
---
lib/models/workdir-context-pool.js | 5 +++
test/models/workdir-context-pool.test.js | 53 +++++++++++++++++++++++-
2 files changed, 57 insertions(+), 1 deletion(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index 647537c263..3e8507a5bd 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -59,6 +59,11 @@ export default class WorkdirContextPool {
context.create();
}
+ replace(directory, options = {}) {
+ this.remove(directory);
+ this.add(directory, options);
+ }
+
remove(directory) {
const existing = this.contexts.get(directory);
this.contexts.delete(directory);
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index b4a10a6e86..38d926127e 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -1,4 +1,6 @@
-import {cloneRepository} from '../helpers';
+import temp from 'temp';
+
+import {cloneRepository, buildRepository} from '../helpers';
import Repository from '../../lib/models/repository';
import WorkdirContextPool from '../../lib/models/workdir-context-pool';
@@ -85,6 +87,55 @@ describe('WorkdirContextPool', function() {
});
});
+ describe('replace', function() {
+ it('adds a WorkdirContext if one is absent', function() {
+ const directory = temp.mkdirSync();
+
+ assert.equal(pool.size(), 0);
+ assert.isFalse(pool.getContext(directory).isPresent());
+
+ pool.replace(directory);
+
+ assert.equal(pool.size(), 1);
+ assert.isTrue(pool.getContext(directory).isPresent());
+ });
+
+ it('removes an existing WorkdirContext if one is present', async function() {
+ const directory = await cloneRepository('three-files');
+
+ pool.add(directory);
+ assert.equal(pool.size(), 1);
+ assert.isTrue(pool.getContext(directory).isPresent());
+ const original = await pool.getContext(directory).getRepositoryPromise();
+
+ pool.replace(directory);
+ assert.equal(pool.size(), 1);
+ assert.isTrue(pool.getContext(directory).isPresent());
+ const replaced = await pool.getContext(directory).getRepositoryPromise();
+
+ assert.notStrictEqual(original, replaced);
+ });
+
+ it('passes a Repository to the created WorkdirContext', async function() {
+ const directory = await cloneRepository('three-files');
+
+ pool.add(directory);
+ assert.equal(pool.size(), 1);
+ assert.isTrue(pool.getContext(directory).isPresent());
+ const original = await pool.getContext(directory).getRepositoryPromise();
+
+ const replacement = await buildRepository(directory);
+
+ pool.replace(directory, {repository: replacement});
+ assert.equal(pool.size(), 1);
+ assert.isTrue(pool.getContext(directory).isPresent());
+ const replaced = await pool.getContext(directory).getRepositoryPromise();
+
+ assert.notStrictEqual(original, replaced);
+ assert.strictEqual(replacement, replaced);
+ });
+ });
+
describe('remove', function() {
let existingDirectory, existingContext;
From 5387fd04651fc9e5ce291d54bf7493eaa3695b1a Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 10:58:09 -0400
Subject: [PATCH 043/161] Invalidate WorkdirCache entries by subdirectory
---
lib/models/workdir-cache.js | 9 ++++++
test/models/workdir-cache.test.js | 46 +++++++++++++++++++++++++++++++
2 files changed, 55 insertions(+)
diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js
index aab82f4f10..3c0c7b1c77 100644
--- a/lib/models/workdir-cache.js
+++ b/lib/models/workdir-cache.js
@@ -34,6 +34,15 @@ export default class WorkdirCache {
}
}
+ async invalidate(baseDir) {
+ const resolvedBase = await this.resolvePath(baseDir);
+ for (const cachedPath of this.known.keys()) {
+ if (cachedPath.startsWith(resolvedBase)) {
+ this.known.delete(cachedPath);
+ }
+ }
+ }
+
resolvePath(unresolvedPath) {
return new Promise((resolve, reject) => {
fs.realpath(unresolvedPath, (err, resolved) => (err ? reject(err) : resolve(resolved)));
diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js
index 8315f3fcaf..31963dcec0 100644
--- a/test/models/workdir-cache.test.js
+++ b/test/models/workdir-cache.test.js
@@ -60,6 +60,52 @@ describe('WorkdirCache', function() {
assert.isFalse(cache.walkToRoot.called);
});
+ it('removes cached entries for all subdirectories of an entry', async function() {
+ const [dir0, dir1] = await Promise.all([
+ cloneRepository('three-files'),
+ cloneRepository('three-files'),
+ ]);
+
+ const pathsToCheck = [
+ dir0,
+ path.join(dir0, 'a.txt'),
+ path.join(dir0, 'subdir-1'),
+ path.join(dir0, 'subdir-1', 'b.txt'),
+ dir1,
+ ];
+ const expectedWorkdirs = [
+ dir0, dir0, dir0, dir0, dir1,
+ ];
+
+ // Prime the cache
+ const initial = await Promise.all(
+ pathsToCheck.map(input => cache.find(input)),
+ );
+ assert.deepEqual(initial, expectedWorkdirs);
+
+ // Re-lookup and hit the cache
+ sinon.spy(cache, 'walkToRoot');
+ const relookup = await Promise.all(
+ pathsToCheck.map(input => cache.find(input)),
+ );
+ assert.deepEqual(relookup, expectedWorkdirs);
+ assert.equal(cache.walkToRoot.callCount, 0);
+
+ // Clear dir0
+ await cache.invalidate(dir0);
+
+ // Re-lookup and hit the cache once
+ const after = await Promise.all(
+ pathsToCheck.map(input => cache.find(input)),
+ );
+ assert.deepEqual(after, expectedWorkdirs);
+ assert.isTrue(cache.walkToRoot.calledWith(dir0));
+ assert.isTrue(cache.walkToRoot.calledWith(path.join(dir0, 'a.txt')));
+ assert.isTrue(cache.walkToRoot.calledWith(path.join(dir0, 'subdir-1')));
+ assert.isTrue(cache.walkToRoot.calledWith(path.join(dir0, 'subdir-1', 'b.txt')));
+ assert.isFalse(cache.walkToRoot.calledWith(dir1));
+ });
+
it('clears the cache when the maximum size is exceeded', async function() {
const dirs = await Promise.all(
Array(6).fill(null, 0, 6).map(() => cloneRepository('three-files')),
From 6648aa1f0f511a93854803108ce48073030ba293 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 11:00:46 -0400
Subject: [PATCH 044/161] Invalidate the workdir cache when an init has been
performed
---
lib/github-package.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 8002be3c3e..5428c0019e 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -207,7 +207,8 @@ export default class GithubPackage {
throw new Error('Must specify a project path to create a repository for');
}
const repository = await Repository.init(projectPath);
- this.contextPool.add(projectPath, {repository});
+ this.contextPool.replace(projectPath, {repository});
+ this.workdirCache.invalidate(projectPath);
await this.scheduleActiveContextUpdate();
}
@@ -215,7 +216,8 @@ export default class GithubPackage {
@autobind
async cloneRepositoryForProjectPath(remoteUrl, projectPath) {
const repository = await Repository.clone(remoteUrl, projectPath);
- this.contextPool.add(projectPath, {repository});
+ this.contextPool.replace(projectPath, {repository});
+ this.workdirCache.invalidate(projectPath);
this.projects.addPath(projectPath);
await this.scheduleActiveContextUpdate();
From e6db0527923ba8dc58d586842f175f21a1a3c99b Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 11:38:08 -0400
Subject: [PATCH 045/161] Rerender when the Repository loads
---
lib/github-package.js | 1 +
lib/models/workdir-context-pool.js | 21 +++++++++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/lib/github-package.js b/lib/github-package.js
index 5428c0019e..8e5f18785e 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -51,6 +51,7 @@ export default class GithubPackage {
this.contextPool.onDidChangeWorkdirOrHead(context => {
this.refreshAtomGitRepository(context.getWorkingDirectory());
}),
+ this.contextPool.onDidLoadRepository(context => context === this.activeContext && this.rerender()),
this.contextPool.onDidUpdateRepository(context => {
this.switchboard.didUpdateRepository(context.getRepository());
}),
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index 3e8507a5bd..23e1cf40b2 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -43,6 +43,7 @@ export default class WorkdirContextPool {
this.contexts.set(directory, context);
const disposable = new CompositeDisposable();
+
const forwardEvent = (subMethod, emitEventName) => {
const emit = () => this.emitter.emit(emitEventName, context);
disposable.add(context[subMethod](emit));
@@ -50,6 +51,14 @@ export default class WorkdirContextPool {
forwardEvent('onDidChangeWorkdirOrHead', 'did-change-workdir-or-head');
forwardEvent('onDidUpdateRepository', 'did-update-repository');
forwardEvent('onDidDestroyRepository', 'did-destroy-repository');
+
+ const forwardPromise = (subMethod, emitEventName) => {
+ context[subMethod]().then(() => this.emitter.emit(emitEventName, context));
+ };
+ forwardPromise('getRepositoryPromise', 'did-load-repository');
+ forwardPromise('getResolutionProgressPromise', 'did-load-resolution-progress');
+ forwardPromise('getChangeObserverPromise', 'did-load-change-observer');
+
disposable.add(this.onDidRemoveContext(removed => {
if (removed === context) {
disposable.dispose();
@@ -96,6 +105,18 @@ export default class WorkdirContextPool {
return this.emitter.on('did-remove-context', callback);
}
+ onDidLoadRepository(callback) {
+ return this.emitter.on('did-load-repository', callback);
+ }
+
+ onDidLoadResolutionProgress(callback) {
+ return this.emitter.on('did-load-resolution-progress', callback);
+ }
+
+ onDidLoadChangeObserver(callback) {
+ return this.emitter.on('did-load-change-observer', callback);
+ }
+
onDidUpdateRepository(callback) {
return this.emitter.on('did-update-repository', callback);
}
From b3a324fa7d6d82dd19ff3f59b7d116b6d5bcc2c8 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 12:07:16 -0400
Subject: [PATCH 046/161] Typo fix
---
lib/github-package.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 8e5f18785e..51e9275009 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -219,7 +219,7 @@ export default class GithubPackage {
const repository = await Repository.clone(remoteUrl, projectPath);
this.contextPool.replace(projectPath, {repository});
this.workdirCache.invalidate(projectPath);
- this.projects.addPath(projectPath);
+ this.project.addPath(projectPath);
await this.scheduleActiveContextUpdate();
}
From 9aab04c693b73f05da09665c80d2fb4249443d6f Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 12:07:44 -0400
Subject: [PATCH 047/161] Wait until change observers have started
---
test/github-package.test.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index de8f34174f..d52d5a46e2 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -411,7 +411,12 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
- await untilRepositories(workdirPath1, workdirPath2);
+ await until('change observers have started', async () => {
+ const observers = await Promise.all([workdirPath1, workdirPath2].map(workdir => {
+ return contextPool.getContext(workdir).getChangeObserverPromise();
+ }));
+ return observers.every(observer => observer && observer.started);
+ });
[atomGitRepository1, atomGitRepository2] = githubPackage.project.getRepositories();
sinon.stub(atomGitRepository1, 'refreshStatus');
From c29ac8c8e79b2942f5f24aa2dcb44162a292fa32 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 13:12:18 -0400
Subject: [PATCH 048/161] Set .started *after* watcher has been started
---
lib/models/file-system-change-observer.js | 2 +-
lib/models/workspace-change-observer.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js
index e87af38d92..870a904048 100644
--- a/lib/models/file-system-change-observer.js
+++ b/lib/models/file-system-change-observer.js
@@ -13,8 +13,8 @@ export default class FileSystemChangeObserver {
}
async start() {
- this.started = true;
await this.watchRepository();
+ this.started = true;
return this;
}
diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js
index ef6f41c083..12bf99d0b9 100644
--- a/lib/models/workspace-change-observer.js
+++ b/lib/models/workspace-change-observer.js
@@ -16,7 +16,6 @@ export default class WorkspaceChangeObserver {
}
async start() {
- this.started = true;
const focusHandler = event => {
if (this.repository) {
this.logger.showEvents([event]);
@@ -30,6 +29,7 @@ export default class WorkspaceChangeObserver {
new Disposable(() => this.window.removeEventListener('focus', focusHandler)),
);
await this.watchActiveRepositoryGitDirectory();
+ this.started = true;
return this;
}
From a1d8522d2a81a3699a12e97de500cadd112f9b29 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 13:15:33 -0400
Subject: [PATCH 049/161] CompositeDisposable.add() no longer supports multiple
arguments
---
lib/models/workspace-change-observer.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js
index 12bf99d0b9..9e5c5d9ce9 100644
--- a/lib/models/workspace-change-observer.js
+++ b/lib/models/workspace-change-observer.js
@@ -23,8 +23,7 @@ export default class WorkspaceChangeObserver {
}
};
this.window.addEventListener('focus', focusHandler);
- this.disposables = new CompositeDisposable();
- this.disposables.add(
+ this.disposables = new CompositeDisposable(
this.workspace.observeTextEditors(this.observeTextEditor),
new Disposable(() => this.window.removeEventListener('focus', focusHandler)),
);
From 9e40126e9ab385de654b74129f29d5371b18bfe9 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 13:34:00 -0400
Subject: [PATCH 050/161] Initialize disposables in the constructor
---
lib/models/workspace-change-observer.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js
index 9e5c5d9ce9..2b3a0ac4fa 100644
--- a/lib/models/workspace-change-observer.js
+++ b/lib/models/workspace-change-observer.js
@@ -12,6 +12,7 @@ export default class WorkspaceChangeObserver {
this.workspace = workspace;
this.observedBuffers = new WeakSet();
this.emitter = new Emitter();
+ this.disposables = new CompositeDisposable();
this.logger = new EventLogger('WorkspaceChangeObserver');
}
@@ -23,7 +24,7 @@ export default class WorkspaceChangeObserver {
}
};
this.window.addEventListener('focus', focusHandler);
- this.disposables = new CompositeDisposable(
+ this.disposables.add(
this.workspace.observeTextEditors(this.observeTextEditor),
new Disposable(() => this.window.removeEventListener('focus', focusHandler)),
);
From b7866ce86c775187fc63eabbcd9c32098ca9bfba Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 14:11:46 -0400
Subject: [PATCH 051/161] Return a Disposable from stubbed method
---
test/models/workdir-context.test.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 3ae8c42223..5066389650 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -1,4 +1,4 @@
-import {CompositeDisposable} from 'event-kit';
+import {CompositeDisposable, Disposable} from 'event-kit';
import {cloneRepository} from '../helpers';
@@ -19,7 +19,7 @@ describe('WorkdirContext', function() {
};
mockWorkspace = {
- observeTextEditors: sinon.spy(),
+ observeTextEditors: sinon.stub().returns(new Disposable()),
};
mockPromptCallback = query => 'reply';
From 6bc97149138e0c9567649f4109bb1b2ad2236313 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 14:20:21 -0400
Subject: [PATCH 052/161] Fully initialize workspace watchers on Linux
---
test/models/workdir-context.test.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 5066389650..34546d689d 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -149,7 +149,12 @@ describe('WorkdirContext', function() {
beforeEach(async function() {
repository = await Repository.open(workingDirectory);
- context = new WorkdirContext(workingDirectory, {repository});
+ context = new WorkdirContext(workingDirectory, {
+ repository,
+ window: mockWindow,
+ workspace: mockWorkspace,
+ promptCallback: mockPromptCallback,
+ });
});
it('returns the repository synchrounsly', function() {
From c0f279e6ec3ced64e3fe0e7945863d5bf3256d9e Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 11 Apr 2017 14:35:16 -0400
Subject: [PATCH 053/161] Instrument context changes and full re-renders
---
lib/github-package.js | 29 ++++++++++++++++++++++++++---
lib/switchboard.js | 4 ++++
2 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 51e9275009..0071bfdd86 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -69,6 +69,7 @@ export default class GithubPackage {
const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk'];
this.subscriptions.add(
+ // Staging and unstaging operations
this.switchboard.onDidBeginStageOperation(payload => {
if (payload.stage && payload.line) {
yardstick.begin('stageLine');
@@ -88,6 +89,20 @@ export default class GithubPackage {
yardstick.finish(stagingSeries);
}
}),
+
+ // Active context changes
+ this.switchboard.onDidScheduleActiveContextUpdate(() => {
+ yardstick.begin('activeContextChange');
+ }),
+ this.switchboard.onDidBeginActiveContextUpdate(() => {
+ yardstick.mark('activeContextChange', 'queue-wait');
+ }),
+ this.switchboard.onDidFinishContextChangeRender(() => {
+ yardstick.mark('activeContextChange', 'render');
+ }),
+ this.switchboard.onDidFinishActiveContextUpdate(() => {
+ yardstick.finish('activeContextChange');
+ }),
);
}
@@ -148,7 +163,7 @@ export default class GithubPackage {
}
@autobind
- rerender() {
+ rerender(callback) {
if (!this.element) {
this.element = document.createElement('div');
this.subscriptions.add(new Disposable(() => {
@@ -174,7 +189,7 @@ export default class GithubPackage {
createRepositoryForProjectPath={this.createRepositoryForProjectPath}
cloneRepositoryForProjectPath={this.cloneRepositoryForProjectPath}
switchboard={this.switchboard}
- />, this.element,
+ />, this.element, callback,
);
}
@@ -242,6 +257,7 @@ export default class GithubPackage {
@autobind
async scheduleActiveContextUpdate(savedState = {}) {
+ this.switchboard.didScheduleActiveContextUpdate();
await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false});
}
@@ -298,11 +314,18 @@ export default class GithubPackage {
setActiveContext(nextActiveContext) {
if (nextActiveContext !== this.activeContext) {
this.activeContext = nextActiveContext;
- this.rerender();
+ this.rerender(() => {
+ this.switchboard.didFinishContextChangeRender();
+ this.switchboard.didFinishActiveContextUpdate();
+ });
+ } else {
+ this.switchboard.didFinishActiveContextUpdate();
}
}
async updateActiveContext(savedState = {}) {
+ this.switchboard.didBeginActiveContextUpdate();
+
const nextActiveContext = await this.getNextContext(savedState);
this.setActiveContext(nextActiveContext);
}
diff --git a/lib/switchboard.js b/lib/switchboard.js
index a00cb14c6c..45a66603b6 100644
--- a/lib/switchboard.js
+++ b/lib/switchboard.js
@@ -58,7 +58,11 @@ export default class Switchboard {
'BeginStageOperation',
'FinishStageOperation',
'ChangePatch',
+ 'ScheduleActiveContextUpdate',
+ 'BeginActiveContextUpdate',
+ 'FinishActiveContextUpdate',
'FinishRender',
+ 'FinishContextChangeRender',
].forEach(eventName => {
Switchboard.prototype[`did${eventName}`] = function(payload) {
this.did(eventName, payload);
From e67fe5cfddc2f2deaa84dcf98bbdc747756ae6a0 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 12 Apr 2017 11:48:34 -0400
Subject: [PATCH 054/161] :fire: unused method
---
lib/models/repository.js | 4 ----
1 file changed, 4 deletions(-)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 6b4a0dc137..ab0f05cd0b 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -355,10 +355,6 @@ export default class Repository {
}
}
- getMergeHead() {
- return this.git.getMergeHead();
- }
-
async getMergeMessage() {
try {
const contents = await readFile(path.join(this.getWorkingDirectoryPath(), '.git', 'MERGE_MSG'), 'utf8');
From 5b30c7ac56f3bcfa5e6fb3cc19b3244bff13be0b Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 12 Apr 2017 11:49:47 -0400
Subject: [PATCH 055/161] Rework Repository tests in anticipation of the state
machine
---
test/models/repository.test.js | 182 ++++++++++++++++++++-------------
1 file changed, 109 insertions(+), 73 deletions(-)
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index 70d1b03441..ab69c46ae1 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -5,7 +5,7 @@ import temp from 'temp';
import Repository from '../../lib/models/repository';
-import {cloneRepository, buildRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers';
+import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers';
describe('Repository', function() {
describe('githubInfoFromRemote', function() {
@@ -43,8 +43,17 @@ describe('Repository', function() {
describe('init', function() {
it('creates a repository in the given dir and returns the repository', async function() {
const soonToBeRepositoryPath = fs.realpathSync(temp.mkdirSync());
- const repo = await Repository.init(soonToBeRepositoryPath);
- assert.isTrue(await repo.isGitRepository());
+ const repo = new Repository(soonToBeRepositoryPath);
+ assert.isTrue(repo.isLoading());
+
+ await repo.getLoadPromise();
+ assert.isTrue(repo.isEmpty());
+
+ const initPromise = repo.init();
+ assert.isTrue(repo.isLoading());
+ await initPromise;
+
+ assert.isTrue(repo.isPresent());
assert.equal(repo.getWorkingDirectoryPath(), soonToBeRepositoryPath);
});
});
@@ -54,8 +63,11 @@ describe('Repository', function() {
const upstreamPath = await cloneRepository('three-files');
const destDir = fs.realpathSync(temp.mkdirSync());
- const repo = await Repository.clone(upstreamPath, destDir);
- assert.isTrue(await repo.isGitRepository());
+ const repo = new Repository(destDir);
+ const clonePromise = repo.clone(upstreamPath, destDir);
+ assert.isTrue(repo.isLoading());
+ await clonePromise;
+ assert.isTrue(repo.isPresent());
assert.equal(repo.getWorkingDirectoryPath(), destDir);
});
@@ -64,8 +76,9 @@ describe('Repository', function() {
const parentDir = fs.realpathSync(temp.mkdirSync());
const destDir = path.join(parentDir, 'subdir');
- const repo = await Repository.clone(upstreamPath, destDir);
- assert.isTrue(await repo.isGitRepository());
+ const repo = new Repository(destDir);
+ await repo.clone(upstreamPath, destDir);
+ assert.isTrue(repo.isPresent());
assert.equal(repo.getWorkingDirectoryPath(), destDir);
});
});
@@ -73,7 +86,9 @@ describe('Repository', function() {
describe('staging and unstaging files', function() {
it('can stage and unstage modified files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
const [patch] = await repo.getUnstagedChanges();
const filePath = patch.filePath;
@@ -91,7 +106,9 @@ describe('Repository', function() {
it('can stage and unstage removed files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.unlinkSync(path.join(workingDirPath, 'subdir-1', 'b.txt'));
const [patch] = await repo.getUnstagedChanges();
const filePath = patch.filePath;
@@ -109,7 +126,9 @@ describe('Repository', function() {
it('can stage and unstage renamed files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.renameSync(path.join(workingDirPath, 'c.txt'), path.join(workingDirPath, 'subdir-1', 'd.txt'));
const patches = await repo.getUnstagedChanges();
const filePath1 = patches[0].filePath;
@@ -129,7 +148,9 @@ describe('Repository', function() {
it('can stage and unstage added files', async function() {
const workingDirPath = await cloneRepository('three-files');
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'e.txt'), 'qux', 'utf8');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
const [patch] = await repo.getUnstagedChanges();
const filePath = patch.filePath;
@@ -146,7 +167,9 @@ describe('Repository', function() {
it('can unstage and retrieve staged changes relative to HEAD~', async function() {
const workingDirPath = await cloneRepository('multiple-commits');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'file.txt'), 'three\nfour\n', 'utf8');
assert.deepEqual(await repo.getStagedChangesSinceParentCommit(), [
{
@@ -194,7 +217,9 @@ describe('Repository', function() {
describe('getFilePatchForPath', function() {
it('returns cached FilePatch objects if they exist', async function() {
const workingDirPath = await cloneRepository('multiple-commits');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'new-file.txt'), 'foooooo', 'utf8');
fs.writeFileSync(path.join(workingDirPath, 'file.txt'), 'qux\nfoo\nbar\n', 'utf8');
await repo.stageFiles(['file.txt']);
@@ -209,7 +234,9 @@ describe('Repository', function() {
it('returns new FilePatch object after repository refresh', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
const filePatchA = await repo.getFilePatchForPath('a.txt');
@@ -224,7 +251,9 @@ describe('Repository', function() {
describe('applyPatchToIndex', function() {
it('can stage and unstage modified files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
const unstagedPatch1 = await repo.getFilePatchForPath(path.join('subdir-1', 'a.txt'));
@@ -256,7 +285,9 @@ describe('Repository', function() {
describe('commit', function() {
it('creates a commit that contains the staged changes', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
assert.equal((await repo.getLastCommit()).message, 'Initial commit');
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
@@ -282,7 +313,9 @@ describe('Repository', function() {
it('amends the last commit when the amend option is set to true', async function() {
const workingDirPath = await cloneRepository('multiple-commits');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
const lastCommit = await repo.git.getHeadCommit();
const lastCommitParent = await repo.git.getCommit('HEAD~');
await repo.commit('amend last commit', {amend: true, allowEmpty: true});
@@ -294,7 +327,9 @@ describe('Repository', function() {
it('throws an error when there are unmerged files', async function() {
const workingDirPath = await cloneRepository('merge-conflict');
- const repository = await buildRepository(workingDirPath);
+ const repository = new Repository(workingDirPath);
+ await repository.getLoadPromise();
+
try {
await repository.git.merge('origin/branch');
} catch (e) {
@@ -317,7 +352,8 @@ describe('Repository', function() {
it('wraps the commit message body at 72 characters', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
await repo.stageFiles([path.join('subdir-1', 'a.txt')]);
@@ -338,7 +374,8 @@ describe('Repository', function() {
it('strips out comments', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
await repo.stageFiles([path.join('subdir-1', 'a.txt')]);
@@ -357,7 +394,9 @@ describe('Repository', function() {
describe('fetch(branchName)', function() {
it('brings commits from the remote and updates remote branch, and does not update branch', async function() {
const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true});
- const localRepo = await buildRepository(localRepoPath);
+ const localRepo = new Repository(localRepoPath);
+ await localRepo.getLoadPromise();
+
let remoteHead, localHead;
remoteHead = await localRepo.git.getCommit('origin/master');
localHead = await localRepo.git.getCommit('master');
@@ -375,7 +414,9 @@ describe('Repository', function() {
describe('pull()', function() {
it('updates the remote branch and merges into local branch', async function() {
const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true});
- const localRepo = await buildRepository(localRepoPath);
+ const localRepo = new Repository(localRepoPath);
+ await localRepo.getLoadPromise();
+
let remoteHead, localHead;
remoteHead = await localRepo.git.getCommit('origin/master');
localHead = await localRepo.git.getCommit('master');
@@ -393,7 +434,8 @@ describe('Repository', function() {
describe('push()', function() {
it('sends commits to the remote and updates ', async function() {
const {localRepoPath, remoteRepoPath} = await setUpLocalAndRemoteRepositories();
- const localRepo = await buildRepository(localRepoPath);
+ const localRepo = new Repository(localRepoPath);
+ await localRepo.getLoadPromise();
let localHead, localRemoteHead, remoteHead;
localHead = await localRepo.git.getCommit('master');
@@ -422,7 +464,8 @@ describe('Repository', function() {
describe('getAheadCount(branchName) and getBehindCount(branchName)', function() {
it('returns the number of commits ahead and behind the remote', async function() {
const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true});
- const localRepo = await buildRepository(localRepoPath);
+ const localRepo = new Repository(localRepoPath);
+ await localRepo.getLoadPromise();
assert.equal(await localRepo.getBehindCount('master'), 0);
assert.equal(await localRepo.getAheadCount('master'), 0);
@@ -439,7 +482,8 @@ describe('Repository', function() {
describe('getRemoteForBranch(branchName)', function() {
it('returns the remote name associated to the supplied branch name, null if none exists', async function() {
const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true});
- const localRepo = await buildRepository(localRepoPath);
+ const localRepo = new Repository(localRepoPath);
+ await localRepo.getLoadPromise();
assert.equal(await localRepo.getRemoteForBranch('master'), 'origin');
await localRepo.git.exec(['remote', 'rename', 'origin', 'foo']);
@@ -454,15 +498,9 @@ describe('Repository', function() {
describe('getMergeConflicts()', function() {
it('returns a promise resolving to an array of MergeConflict objects', async function() {
const workingDirPath = await cloneRepository('merge-conflict');
- const repo = await buildRepository(workingDirPath);
- try {
- await repo.git.merge('origin/branch');
- } catch (e) {
- // expected
- if (!e.message.match(/CONFLICT/)) {
- throw new Error(`merge failed for wrong reason: ${e.message}`);
- }
- }
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+ await assert.isRejected(repo.git.merge('origin/branch'), /CONFLICT/);
repo.refresh();
let mergeConflicts = await repo.getMergeConflicts();
@@ -520,7 +558,8 @@ describe('Repository', function() {
it('returns an empty array if the repo has no merge conflicts', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
const mergeConflicts = await repo.getMergeConflicts();
assert.deepEqual(mergeConflicts, []);
@@ -530,12 +569,9 @@ describe('Repository', function() {
describe('stageFiles([path])', function() {
it('updates the staged changes accordingly', async function() {
const workingDirPath = await cloneRepository('merge-conflict');
- const repo = await buildRepository(workingDirPath);
- try {
- await repo.git.merge('origin/branch');
- } catch (e) {
- // expected
- }
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+ await assert.isRejected(repo.git.merge('origin/branch'));
repo.refresh();
const mergeConflictPaths = (await repo.getMergeConflicts()).map(c => c.filePath);
@@ -584,12 +620,9 @@ describe('Repository', function() {
describe('pathHasMergeMarkers()', function() {
it('returns true if and only if the file has merge markers', async function() {
const workingDirPath = await cloneRepository('merge-conflict');
- const repo = await buildRepository(workingDirPath);
- try {
- await repo.git.merge('origin/branch');
- } catch (e) {
- // expected
- }
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+ await assert.isRejected(repo.git.merge('origin/branch'));
assert.isTrue(await repo.pathHasMergeMarkers('added-to-both.txt'));
assert.isFalse(await repo.pathHasMergeMarkers('removed-on-master.txt'));
@@ -625,12 +658,11 @@ describe('Repository', function() {
describe('when the working directory is clean', function() {
it('resets the index and the working directory to match HEAD', async function() {
const workingDirPath = await cloneRepository('merge-conflict-abort');
- const repo = await buildRepository(workingDirPath);
- try {
- await repo.git.merge('origin/spanish');
- } catch (e) {
- // expected
- }
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
+ await assert.isRejected(repo.git.merge('origin/spanish'));
+
assert.equal(await repo.isMerging(), true);
await repo.abortMerge();
assert.equal(await repo.isMerging(), false);
@@ -640,12 +672,9 @@ describe('Repository', function() {
describe('when a dirty file in the working directory is NOT under conflict', function() {
it('successfully aborts the merge and does not affect the dirty file', async function() {
const workingDirPath = await cloneRepository('merge-conflict-abort');
- const repo = await buildRepository(workingDirPath);
- try {
- await repo.git.merge('origin/spanish');
- } catch (e) {
- // expected
- }
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+ await assert.isRejected(repo.git.merge('origin/spanish'));
fs.writeFileSync(path.join(workingDirPath, 'fruit.txt'), 'a change\n');
assert.equal(await repo.isMerging(), true);
@@ -661,12 +690,9 @@ describe('Repository', function() {
describe('when a dirty file in the working directory is under conflict', function() {
it('throws an error indicating that the abort could not be completed', async function() {
const workingDirPath = await cloneRepository('merge-conflict-abort');
- const repo = await buildRepository(workingDirPath);
- try {
- await repo.git.merge('origin/spanish');
- } catch (e) {
- // expected
- }
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+ await assert.isRejected(repo.git.merge('origin/spanish'));
fs.writeFileSync(path.join(workingDirPath, 'animal.txt'), 'a change\n');
const stagedChanges = await repo.getStagedChanges();
@@ -674,7 +700,7 @@ describe('Repository', function() {
assert.equal(await repo.isMerging(), true);
try {
- await repo.abortMerge();
+ await assert.isRejected(repo.abortMerge(), /^git merge --abort/);
assert(false);
} catch (e) {
assert.match(e.command, /^git merge --abort/);
@@ -690,7 +716,9 @@ describe('Repository', function() {
describe('discardWorkDirChangesForPaths()', function() {
it('can discard working directory changes in modified files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'b.txt'), 'qux\nfoo\nbar\n', 'utf8');
fs.writeFileSync(path.join(workingDirPath, 'new-file.txt'), 'hello there', 'utf8');
@@ -704,7 +732,9 @@ describe('Repository', function() {
it('can discard working directory changes in removed files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.unlinkSync(path.join(workingDirPath, 'subdir-1', 'a.txt'));
fs.unlinkSync(path.join(workingDirPath, 'subdir-1', 'b.txt'));
const unstagedChanges = await repo.getUnstagedChanges();
@@ -717,7 +747,9 @@ describe('Repository', function() {
it('can discard working directory changes added files', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo = await buildRepository(workingDirPath);
+ const repo = new Repository(workingDirPath);
+ await repo.getLoadPromise();
+
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'e.txt'), 'qux', 'utf8');
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'f.txt'), 'qux', 'utf8');
const unstagedChanges = await repo.getUnstagedChanges();
@@ -732,7 +764,8 @@ describe('Repository', function() {
describe('maintaining discard history across repository instances', function() {
it('restores the history', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo1 = await buildRepository(workingDirPath);
+ const repo1 = new Repository(workingDirPath);
+ await repo1.getLoadPromise();
fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
fs.writeFileSync(path.join(workingDirPath, 'b.txt'), 'woohoo', 'utf8');
@@ -748,7 +781,8 @@ describe('Repository', function() {
});
const repo1HistorySha = repo1.createDiscardHistoryBlob();
- const repo2 = await buildRepository(workingDirPath);
+ const repo2 = new Repository(workingDirPath);
+ await repo2.getLoadPromise();
const repo2HistorySha = repo2.createDiscardHistoryBlob();
assert.deepEqual(repo2HistorySha, repo1HistorySha);
@@ -756,14 +790,16 @@ describe('Repository', function() {
it('is resilient to missing history blobs', async function() {
const workingDirPath = await cloneRepository('three-files');
- const repo1 = await buildRepository(workingDirPath);
+ const repo1 = new Repository(workingDirPath);
+ await repo1.getLoadPromise();
await repo1.setConfig('atomGithub.historySha', '1111111111111111111111111111111111111111');
// Should not throw
await repo1.updateDiscardHistory();
// Also should not throw
- await buildRepository(workingDirPath);
+ const repo2 = new Repository(workingDirPath);
+ await repo2.getLoadPromise();
});
});
});
From 4a5a6e771425078bb2c0780bbb68fc2b195ea62e Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 12 Apr 2017 11:50:27 -0400
Subject: [PATCH 056/161] Additional models to return from Repository methods
---
lib/models/branch.js | 23 ++++++++++++++++++++++
lib/models/commit.js | 47 ++++++++++++++++++++++++++++++++++++++++++++
lib/models/remote.js | 23 ++++++++++++++++++++++
3 files changed, 93 insertions(+)
create mode 100644 lib/models/branch.js
create mode 100644 lib/models/commit.js
create mode 100644 lib/models/remote.js
diff --git a/lib/models/branch.js b/lib/models/branch.js
new file mode 100644
index 0000000000..9047086b5b
--- /dev/null
+++ b/lib/models/branch.js
@@ -0,0 +1,23 @@
+export default class Branch {
+ constructor(name) {
+ this.name = name;
+ }
+
+ getName() {
+ return this.name;
+ }
+
+ isPresent() {
+ return true;
+ }
+}
+
+export const nullBranch = {
+ getName() {
+ return '';
+ },
+
+ isPresent() {
+ return false;
+ },
+};
diff --git a/lib/models/commit.js b/lib/models/commit.js
new file mode 100644
index 0000000000..f7398c5a8c
--- /dev/null
+++ b/lib/models/commit.js
@@ -0,0 +1,47 @@
+const UNBORN = {};
+
+export default class Commit {
+ static createUnborn() {
+ return new Commit('', '', UNBORN);
+ }
+
+ constructor(sha, message, unbornRef = null) {
+ this.sha = sha;
+ this.message = message.trim();
+ this.unbornRef = unbornRef === UNBORN;
+ }
+
+ getSha() {
+ return this.sha;
+ }
+
+ getMessage() {
+ return this.message;
+ }
+
+ isUnbornRef() {
+ return this.unbornRef;
+ }
+
+ isPresent() {
+ return true;
+ }
+}
+
+export const nullCommit = {
+ getSha() {
+ return '';
+ },
+
+ getMessage() {
+ return '';
+ },
+
+ isUnbornRef() {
+ return false;
+ },
+
+ isPresent() {
+ return false;
+ },
+};
diff --git a/lib/models/remote.js b/lib/models/remote.js
new file mode 100644
index 0000000000..05d237faa9
--- /dev/null
+++ b/lib/models/remote.js
@@ -0,0 +1,23 @@
+export default class Remote {
+ constructor(name) {
+ this.name = name;
+ }
+
+ getName() {
+ return this.name;
+ }
+
+ isPresent() {
+ return true;
+ }
+}
+
+export const nullRemote = {
+ getName() {
+ return '';
+ },
+
+ isPresent() {
+ return false;
+ },
+};
From 8cd8c5b4f2b3dc40b64672a0f1ef8c52ae6c7e07 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 09:28:37 -0400
Subject: [PATCH 057/161] `mkdirs` helper to promisify `fs.mkdirs`
---
lib/helpers.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/lib/helpers.js b/lib/helpers.js
index 9cdbdd5b41..c43a51943d 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -170,6 +170,12 @@ export async function isFileExecutable(absoluteFilePath) {
return stat.mode & fs.constants.S_IXUSR; // eslint-disable-line no-bitwise
}
+export function mkdirs(directory) {
+ return new Promise((resolve, reject) => {
+ fs.mkdirs(directory, err => (err ? reject(err) : resolve()));
+ });
+}
+
export function shortenSha(sha) {
return sha.slice(0, 8);
}
From 8ff58fa13eedb68b9c724c6a269d0448aeeb9db3 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 09:29:13 -0400
Subject: [PATCH 058/161] Test case to verify State delegates
---
test/models/repository.test.js | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index ab69c46ae1..b838d1f8db 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -2,12 +2,31 @@ import fs from 'fs';
import path from 'path';
import dedent from 'dedent-js';
import temp from 'temp';
+import util from 'util';
import Repository from '../../lib/models/repository';
+import {expectedDelegates} from '../../lib/models/repository-states';
import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers';
describe('Repository', function() {
+ it('delegates all state methods', function() {
+ const missing = expectedDelegates.filter(delegateName => {
+ return Repository.prototype[delegateName] === undefined;
+ });
+
+ // For convenience, write the delegate list to the console when there are any missing (in addition to failing the
+ // test.)
+ if (missing.length > 0) {
+ const formatted = util.inspect(expectedDelegates);
+
+ // eslint-disable-next-line no-console
+ console.log(`Expected delegates for your copy-and-paste convenience:\n\n---\n${formatted}\n---\n`);
+ }
+
+ assert.lengthOf(missing, 0);
+ });
+
describe('githubInfoFromRemote', function() {
it('returns info about a GitHub repo based on the remote URL', function() {
const atomRepo = {
From 862d60e895440dbd19a3bf320a43e3e27f86a5b8 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 09:29:40 -0400
Subject: [PATCH 059/161] `.clone()` takes one argument
---
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 b838d1f8db..c48e8bae8c 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -83,7 +83,7 @@ describe('Repository', function() {
const destDir = fs.realpathSync(temp.mkdirSync());
const repo = new Repository(destDir);
- const clonePromise = repo.clone(upstreamPath, destDir);
+ const clonePromise = repo.clone(upstreamPath);
assert.isTrue(repo.isLoading());
await clonePromise;
assert.isTrue(repo.isPresent());
From be7672dde41e3e4ee8f08883e795e0140662024d Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 09:30:10 -0400
Subject: [PATCH 060/161] Delegate ALL THE THINGS to the current State
---
lib/models/repository.js | 563 +++++++++------------------------------
1 file changed, 126 insertions(+), 437 deletions(-)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index ab0f05cd0b..5eb8ff21b5 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -1,54 +1,13 @@
-import fs from 'fs-extra';
import path from 'path';
-import {Emitter, Disposable} from 'event-kit';
-import {autobind} from 'core-decorators';
+import {Emitter} from 'event-kit';
import CompositeGitStrategy from '../composite-git-strategy';
-import FilePatch from './file-patch';
-import Hunk from './hunk';
-import HunkLine from './hunk-line';
-import DiscardHistory from './discard-history';
-import {readFile, deleteFileOrFolder} from '../helpers';
+import {readFile} from '../helpers';
-const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
-
-function partition(array, predicate) {
- const matches = [];
- const nonmatches = [];
- array.forEach(item => {
- if (predicate(item)) {
- matches.push(item);
- } else {
- nonmatches.push(item);
- }
- });
- return [matches, nonmatches];
-}
-
-async function loadHistoryPayload(git) {
- const historySha = await git.getConfig('atomGithub.historySha');
- if (!historySha) {
- return {};
- }
-
- let blob;
- try {
- blob = await git.getBlobContents(historySha);
- } catch (e) {
- if (/Not a valid object name/.test(e.stdErr)) {
- return {};
- }
-
- throw e;
- }
+import {InitialState} from './repository-states';
- try {
- return JSON.parse(blob);
- } catch (e) {
- return {};
- }
-}
+const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
export class AbortMergeError extends Error {
constructor(code, filePath) {
@@ -70,32 +29,6 @@ export class CommitError extends Error {
}
export default class Repository {
- static async open(workingDirectoryPath, gitStrategy) {
- const strategy = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
- if (await strategy.isGitRepository()) {
- const history = await loadHistoryPayload(strategy);
- return new Repository(workingDirectoryPath, strategy, history);
- } else {
- return null;
- }
- }
-
- static async init(workingDirectoryPath, gitStrategy) {
- const strategy = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
- await strategy.init(workingDirectoryPath);
- return this.open(workingDirectoryPath, strategy);
- }
-
- static async clone(remoteUrl, destDirectory, gitStrategy) {
- await new Promise((resolve, reject) => {
- fs.mkdirs(destDirectory, err => (err ? reject(err) : resolve()));
- });
-
- const strategy = gitStrategy || CompositeGitStrategy.create(destDirectory);
- await strategy.clone(remoteUrl, {recursive: true});
- return this.open(destDirectory, strategy);
- }
-
static githubInfoFromRemote(remote) {
// proto login domain owner repo
const regex = /(?:.+:\/\/)?(?:.+@)?github\.com[:/]([^/]+)\/(.+)/;
@@ -115,236 +48,68 @@ export default class Repository {
}
}
- constructor(workingDirectoryPath, gitStrategy, history) {
+ constructor(workingDirectoryPath, gitStrategy = null) {
this.workingDirectoryPath = workingDirectoryPath;
- this.emitter = new Emitter();
- this.stagedFilePatchesByPath = new Map();
- this.stagedFilePatchesSinceParentCommitByPath = new Map();
- this.unstagedFilePatchesByPath = new Map();
- this.git = gitStrategy;
- this.destroyed = false;
- this.discardHistory = new DiscardHistory(
- this.createBlob,
- this.expandBlobToFile,
- this.mergeFile,
- this.workingDirectoryPath,
- {maxHistoryLength: 60},
- );
- if (history) { this.discardHistory.updateHistory(history); }
- const onWindowFocus = () => this.updateDiscardHistory();
- window.addEventListener('focus', onWindowFocus);
- this.windowFocusDisposable = new Disposable(() => window.removeEventListener('focus', onWindowFocus));
- }
-
- /*
- * Use `callback` to request user input from all git strategies.
- */
- setPromptCallback(callback) {
- this.git.getImplementers().forEach(strategy => strategy.setPromptCallback(callback));
- }
-
- destroy() {
- this.destroyed = true;
- this.emitter.emit('did-destroy');
- this.emitter.dispose();
- this.windowFocusDisposable.dispose();
- }
+ this.git = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
- isDestroyed() {
- return this.destroyed;
- }
-
- onDidDestroy(callback) {
- return this.emitter.on('did-destroy', callback);
- }
-
- onDidUpdate(callback) {
- return this.emitter.on('did-update', callback);
- }
-
- didUpdate() {
- this.emitter.emit('did-update');
- }
-
- getWorkingDirectoryPath() {
- return this.workingDirectoryPath;
- }
-
- getGitDirectoryPath() {
- return path.join(this.getWorkingDirectoryPath(), '.git');
- }
-
- refresh() {
- /* eslint-disable no-console */
- this.clearCachedFilePatches();
- this.fileStatusesPromise = null;
- this.stagedChangesSinceParentCommitPromise = null;
- this.didUpdate();
- /* eslint-enable no-console */
- }
-
- isGitRepository() {
- return this.git.isGitRepository();
- }
-
- fetchStatusesForChangedFiles() {
- return this.git.getStatusesForChangedFiles();
- }
-
- getStatusesForChangedFiles() {
- if (!this.fileStatusesPromise) {
- this.fileStatusesPromise = this.fetchStatusesForChangedFiles();
- }
- return this.fileStatusesPromise;
- }
-
- async getUnstagedChanges() {
- const {unstagedFiles} = await this.getStatusesForChangedFiles();
- return Object.keys(unstagedFiles).map(filePath => { return {filePath, status: unstagedFiles[filePath]}; });
- }
-
- async getStagedChanges() {
- const {stagedFiles} = await this.getStatusesForChangedFiles();
- return Object.keys(stagedFiles).map(filePath => { return {filePath, status: stagedFiles[filePath]}; });
- }
+ this.emitter = new Emitter();
- async getMergeConflicts() {
- const {mergeConflictFiles} = await this.getStatusesForChangedFiles();
- return Object.keys(mergeConflictFiles).map(filePath => {
- return {filePath, status: mergeConflictFiles[filePath]};
+ this.loadPromise = new Promise(resolve => {
+ const sub = this.onDidChangeState(() => {
+ if (!this.isLoading()) {
+ resolve();
+ sub.dispose();
+ } else if (this.isDestroyed()) {
+ sub.dispose();
+ }
+ });
});
- }
-
- async fetchStagedChangesSinceParentCommit() {
- try {
- const stagedFiles = await this.git.diffFileStatus({staged: true, target: 'HEAD~'});
- return Object.keys(stagedFiles).map(filePath => { return {filePath, status: stagedFiles[filePath]}; });
- } catch (e) {
- if (e.message.includes('ambiguous argument \'HEAD~\'')) {
- return [];
- } else {
- throw e;
- }
- }
- }
- getStagedChangesSinceParentCommit() {
- if (!this.stagedChangesSinceParentCommitPromise) {
- this.stagedChangesSinceParentCommitPromise = this.fetchStagedChangesSinceParentCommit();
- }
- return this.stagedChangesSinceParentCommitPromise;
+ this.transitionTo(InitialState);
}
- clearCachedFilePatches() {
- this.stagedFilePatchesByPath = new Map();
- this.stagedFilePatchesSinceParentCommitByPath = new Map();
- this.unstagedFilePatchesByPath = new Map();
- }
+ // State management //////////////////////////////////////////////////////////////////////////////////////////////////
- isPartiallyStaged(filePath) {
- return this.git.isPartiallyStaged(filePath);
- }
+ transition(currentState, StateConstructor, ...payload) {
+ const nextState = new StateConstructor(this, ...payload);
+ this.state = nextState;
- getCachedFilePatchForPath(filePath, {staged, amending} = {}) {
- if (staged && amending) {
- return this.stagedFilePatchesSinceParentCommitByPath.get(filePath);
- } else if (staged) {
- return this.stagedFilePatchesByPath.get(filePath);
- } else {
- return this.unstagedFilePatchesByPath.get(filePath);
- }
- }
+ this.emitter.emit('did-change-state', {from: currentState, to: this.state});
- cacheFilePatchForPath(filePath, filePatch, {staged, amending} = {}) {
- if (staged && amending) {
- this.stagedFilePatchesSinceParentCommitByPath.set(filePath, filePatch);
- } else if (staged) {
- this.stagedFilePatchesByPath.set(filePath, filePatch);
- } else {
- this.unstagedFilePatchesByPath.set(filePath, filePatch);
- }
+ return this.state.start();
}
- async getFilePatchForPath(filePath, options = {}) {
- const cachedFilePatch = this.getCachedFilePatchForPath(filePath, options);
- if (cachedFilePatch) { return cachedFilePatch; }
-
- if (options.amending) { options.baseCommit = 'HEAD~'; }
- const rawDiff = await this.git.getDiffForFilePath(filePath, options);
- if (rawDiff) {
- const [filePatch] = this.buildFilePatchesFromRawDiffs([rawDiff]);
- this.cacheFilePatchForPath(filePath, filePatch, options);
- return filePatch;
- } else {
- return null;
- }
+ transitionTo(StateConstructor, ...payload) {
+ return this.transition(this.state, StateConstructor, ...payload);
}
- buildFilePatchesFromRawDiffs(rawDiffs) {
- let diffLineNumber = 0;
- return rawDiffs.map(patch => {
- const hunks = patch.hunks.map(hunk => {
- let oldLineNumber = hunk.oldStartLine;
- let newLineNumber = hunk.newStartLine;
- const hunkLines = hunk.lines.map(line => {
- const status = HunkLine.statusMap[line[0]];
- const text = line.slice(1);
- let hunkLine;
- if (status === 'unchanged') {
- hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber, diffLineNumber++);
- oldLineNumber++;
- newLineNumber++;
- } else if (status === 'added') {
- hunkLine = new HunkLine(text, status, -1, newLineNumber, diffLineNumber++);
- newLineNumber++;
- } else if (status === 'deleted') {
- hunkLine = new HunkLine(text, status, oldLineNumber, -1, diffLineNumber++);
- oldLineNumber++;
- } else if (status === 'nonewline') {
- hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++);
- } else {
- throw new Error(`unknow status type: ${status}`);
- }
- return hunkLine;
- });
- return new Hunk(
- hunk.oldStartLine,
- hunk.newStartLine,
- hunk.oldLineCount,
- hunk.newLineCount,
- hunk.heading,
- hunkLines,
- );
- });
- return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks);
- });
+ getLoadPromise() {
+ return this.loadPromise;
}
- async stageFiles(paths) {
- await this.git.stageFiles(paths);
+ /*
+ * Use `callback` to request user input from all git strategies.
+ */
+ setPromptCallback(callback) {
+ this.git.getImplementers().forEach(strategy => strategy.setPromptCallback(callback));
}
- async unstageFiles(paths) {
- await this.git.unstageFiles(paths);
- }
+ // Event subscription ////////////////////////////////////////////////////////////////////////////////////////////////
- async stageFilesFromParentCommit(paths) {
- await this.git.unstageFiles(paths, 'HEAD~');
+ onDidDestroy(callback) {
+ return this.emitter.on('did-destroy', callback);
}
- async applyPatchToIndex(filePatch) {
- const patchStr = filePatch.getHeaderString() + filePatch.toString();
- await this.git.applyPatch(patchStr, {index: true});
+ onDidChangeState(callback) {
+ return this.emitter.on('did-change-state', callback);
}
- async applyPatchToWorkdir(filePatch) {
- const patchStr = filePatch.getHeaderString() + filePatch.toString();
- await this.git.applyPatch(patchStr);
+ onDidUpdate(callback) {
+ return this.emitter.on('did-update', callback);
}
- async checkoutPathsAtRevision(paths, revision = 'HEAD') {
- await this.git.checkoutFiles(paths, revision);
- }
+ // State-independent actions /////////////////////////////////////////////////////////////////////////////////////////
+ // Actions that use direct filesystem access or otherwise don't need `this.git` to be available.
async pathHasMergeMarkers(relativePath) {
try {
@@ -364,200 +129,124 @@ export default class Repository {
}
}
- async abortMerge() {
- await this.git.abortMerge();
- return this.refresh();
- }
+ // State-independent accessors ///////////////////////////////////////////////////////////////////////////////////////
- checkoutSide(side, paths) {
- return this.git.checkoutSide(side, paths);
+ getWorkingDirectoryPath() {
+ return this.workingDirectoryPath;
}
- isMerging() {
- return this.git.isMerging();
+ getGitDirectoryPath() {
+ return path.join(this.getWorkingDirectoryPath(), '.git');
}
- isRebasing() {
- return this.git.isRebasing();
- }
+ // Compound Getters //////////////////////////////////////////////////////////////////////////////////////////////////
+ // Accessor methods for data derived from other, state-provided getters.
- async commit(message, options) {
- await this.git.commit(this.formatCommitMessage(message), options);
- this.stagedChangesPromise = null;
- this.stagedChangesSinceParentCommitPromise = null;
+ async getUnstagedChanges() {
+ const {unstagedFiles} = await this.getStatusesForChangedFiles();
+ return Object.keys(unstagedFiles).map(filePath => { return {filePath, status: unstagedFiles[filePath]}; });
}
- getLastCommit() {
- return this.git.getHeadCommit();
+ async getStagedChanges() {
+ const {stagedFiles} = await this.getStatusesForChangedFiles();
+ return Object.keys(stagedFiles).map(filePath => { return {filePath, status: stagedFiles[filePath]}; });
}
- formatCommitMessage(message) {
- // strip out comments
- const messageWithoutComments = message.replace(/^#.*$/mg, '').trim();
-
- // hard wrap message (except for first line) at 72 characters
- let results = [];
- messageWithoutComments.split('\n').forEach((line, index) => {
- if (line.length <= 72 || index === 0) {
- results.push(line);
- } else {
- const matches = line.match(/.{1,72}(\s|$)|\S+?(\s|$)/g)
- .map(match => {
- return match.endsWith('\n') ? match.substr(0, match.length - 1) : match;
- });
- results = results.concat(matches);
- }
+ async getMergeConflicts() {
+ const {mergeConflictFiles} = await this.getStatusesForChangedFiles();
+ return Object.keys(mergeConflictFiles).map(filePath => {
+ return {filePath, status: mergeConflictFiles[filePath]};
});
-
- return results.join('\n');
}
- readFileFromIndex(filePath) {
- return this.git.readFileFromIndex(filePath);
- }
-
- push(branchName, options) {
- return this.git.push(branchName, options);
- }
-
- fetch(branchName) {
- return this.git.fetch(branchName);
+ async saveDiscardHistory() {
+ const historySha = await this.createDiscardHistoryBlob();
+ await this.setConfig('atomGithub.historySha', historySha);
}
+}
- pull(branchName) {
- return this.git.pull(branchName);
- }
+// The methods named here will be delegated to the current State.
+// This list should match the methods decorated with @shouldDelegate in `lib/models/repository-states/state.js`. A test
+// case in `test/models/repository.test.js` ensures that these sets match.
+const delegates = [
+ 'isLoading',
+ 'isEmpty',
+ 'isPresent',
+ 'isDestroyed',
- getAheadCount(branchName) {
- return this.git.getAheadCount(branchName);
- }
+ 'init',
+ 'clone',
+ 'destroy',
+ 'refresh',
- getBehindCount(branchName) {
- return this.git.getBehindCount(branchName);
- }
+ 'stageFiles',
+ 'unstageFiles',
+ 'stageFilesFromParentCommit',
+ 'applyPatchToIndex',
+ 'applyPatchToWorkdir',
- getRemoteForBranch(branchName) {
- return this.git.getRemoteForBranch(branchName);
- }
+ 'commit',
- getCurrentBranch() {
- return this.git.getCurrentBranch();
- }
+ 'merge',
+ 'abortMerge',
+ 'checkoutSide',
+ 'mergeFile',
+ 'writeMergeConflictToIndex',
- getBranches() {
- return this.git.getBranches();
- }
+ 'checkout',
+ 'checkoutPathsAtRevision',
- getRemotes() {
- return this.git.getRemotes();
- }
+ 'fetch',
+ 'pull',
+ 'push',
- getConfig(configOption, options) {
- return this.git.getConfig(configOption, options);
- }
+ 'setConfig',
- @autobind
- setConfig(configOption, configValue, options) {
- return this.git.setConfig(configOption, configValue, options);
- }
+ 'createBlob',
+ 'expandBlobToFile',
- async checkout(branchName, options) {
- await this.git.checkout(branchName, options);
- }
+ 'createDiscardHistoryBlob',
+ 'saveDiscardHistory',
+ 'updateDiscardHistory',
+ 'storeBeforeAndAfterBlobs',
+ 'restoreLastDiscardInTempFiles',
+ 'popDiscardHistory',
+ 'clearDiscardHistory',
+ 'discardWorkDirChangesForPaths',
- setCommitState(state) {
- this.commitState = state;
- }
+ 'getStatusesForChangedFiles',
+ 'getStagedChangesSinceParentCommit',
+ 'isPartiallyStaged',
+ 'getFilePatchForPath',
+ 'readFileFromIndex',
- getCommitState() {
- return this.commitState;
- }
+ 'getLastCommit',
- @autobind
- createBlob(options = {}) {
- return this.git.createBlob(options);
- }
+ 'getBranches',
+ 'getCurrentBranch',
- @autobind
- expandBlobToFile(absFilePath, sha) {
- return this.git.expandBlobToFile(absFilePath, sha);
- }
+ 'isMerging',
+ 'isRebasing',
- @autobind
- getBlobContents(sha) {
- return this.git.getBlobContents(sha);
- }
+ 'getRemotes',
+ 'getRemoteForBranch',
- @autobind
- mergeFile(oursPath, commonBasePath, theirsPath, resultPath) {
- return this.git.mergeFile(oursPath, commonBasePath, theirsPath, resultPath);
- }
+ 'getAheadCount',
+ 'getBehindCount',
- writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha) {
- return this.git.writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha);
- }
+ 'getConfig',
- async createDiscardHistoryBlob() {
- return await this.discardHistory.createHistoryBlob();
- }
+ 'getBlobContents',
- async saveDiscardHistory() {
- const historySha = await this.createDiscardHistoryBlob();
- await this.setConfig('atomGithub.historySha', historySha);
- }
+ 'hasDiscardHistory',
+ 'getDiscardHistory',
+ 'getLastHistorySnapshots',
+];
- async updateDiscardHistory() {
- const history = await loadHistoryPayload(this.git);
- this.discardHistory.updateHistory(history);
- }
+for (let i = 0; i < delegates.length; i++) {
+ const delegate = delegates[i];
- async storeBeforeAndAfterBlobs(filePaths, isSafe, destructiveAction, partialDiscardFilePath = null) {
- const snapshots = await this.discardHistory.storeBeforeAndAfterBlobs(
- filePaths,
- isSafe,
- destructiveAction,
- partialDiscardFilePath,
- );
- if (snapshots) { await this.saveDiscardHistory(); }
- return snapshots;
- }
-
- async restoreLastDiscardInTempFiles(isSafe, partialDiscardFilePath = null) {
- return await this.discardHistory.restoreLastDiscardInTempFiles(isSafe, partialDiscardFilePath);
- }
-
- async popDiscardHistory(partialDiscardFilePath = null) {
- const removed = await this.discardHistory.popHistory(partialDiscardFilePath);
- if (removed) { await this.saveDiscardHistory(); }
- }
-
- async clearDiscardHistory(partialDiscardFilePath = null) {
- this.discardHistory.clearHistory(partialDiscardFilePath);
- await this.saveDiscardHistory();
- }
-
- hasDiscardHistory(partialDiscardFilePath = null) {
- return this.discardHistory.hasHistory(partialDiscardFilePath);
- }
-
- @autobind
- getDiscardHistory(partialDiscardFilePath = null) {
- return this.discardHistory.getHistory(partialDiscardFilePath);
- }
-
- @autobind
- getLastHistorySnapshots(partialDiscardFilePath = null) {
- return this.discardHistory.getLastSnapshots(partialDiscardFilePath);
- }
-
- @autobind
- async discardWorkDirChangesForPaths(paths) {
- const untrackedFiles = await this.git.getUntrackedFiles();
- const [filesToRemove, filesToCheckout] = partition(paths, f => untrackedFiles.includes(f));
- await this.git.checkoutFiles(filesToCheckout);
- await Promise.all(filesToRemove.map(filePath => {
- const absPath = path.join(this.getWorkingDirectoryPath(), filePath);
- return deleteFileOrFolder(absPath);
- }));
- }
+ Repository.prototype[delegate] = function(...args) {
+ return this.state[delegate](...args);
+ };
}
From 8e44534638f0eed13e47b312ed0d61fba744fc37 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 09:30:54 -0400
Subject: [PATCH 061/161] Repository state machine :wrench:
---
lib/models/repository-states/cloning.js | 21 +
lib/models/repository-states/destroyed.js | 16 +
lib/models/repository-states/empty.js | 21 +
lib/models/repository-states/index.js | 10 +
lib/models/repository-states/initializing.js | 13 +
lib/models/repository-states/loading.js | 32 ++
lib/models/repository-states/present.js | 426 ++++++++++++++++++
lib/models/repository-states/state.js | 430 +++++++++++++++++++
8 files changed, 969 insertions(+)
create mode 100644 lib/models/repository-states/cloning.js
create mode 100644 lib/models/repository-states/destroyed.js
create mode 100644 lib/models/repository-states/empty.js
create mode 100644 lib/models/repository-states/index.js
create mode 100644 lib/models/repository-states/initializing.js
create mode 100644 lib/models/repository-states/loading.js
create mode 100644 lib/models/repository-states/present.js
create mode 100644 lib/models/repository-states/state.js
diff --git a/lib/models/repository-states/cloning.js b/lib/models/repository-states/cloning.js
new file mode 100644
index 0000000000..c92d7af0f1
--- /dev/null
+++ b/lib/models/repository-states/cloning.js
@@ -0,0 +1,21 @@
+import {mkdirs} from '../../helpers';
+
+import State from './state';
+import Loading from './loading';
+
+export default class Cloning extends Loading {
+ constructor(repository, remoteUrl) {
+ super(repository);
+ this.remoteUrl = remoteUrl;
+ }
+
+ async start() {
+ await mkdirs(this.workdir());
+ await this.git().clone(this.remoteUrl, {recursive: true});
+
+ // See https://github.com/babel/babel/issues/3930
+ await Loading.prototype.start.call(this);
+ }
+}
+
+State.register(Cloning);
diff --git a/lib/models/repository-states/destroyed.js b/lib/models/repository-states/destroyed.js
new file mode 100644
index 0000000000..cf48c7b252
--- /dev/null
+++ b/lib/models/repository-states/destroyed.js
@@ -0,0 +1,16 @@
+import State from './state';
+
+export default class Destroyed extends State {
+ start() {
+ this.didDestroy();
+ this.repository.emitter.dispose();
+ }
+
+ isDestroyed() {
+ return true;
+ }
+
+ destroy() {
+ // No-op to destroy twice
+ }
+}
diff --git a/lib/models/repository-states/empty.js b/lib/models/repository-states/empty.js
new file mode 100644
index 0000000000..c3f4039dd6
--- /dev/null
+++ b/lib/models/repository-states/empty.js
@@ -0,0 +1,21 @@
+import State from './state';
+
+export default class Empty extends State {
+ isEmpty() {
+ return true;
+ }
+
+ init() {
+ return this.transitionTo('Initializing');
+ }
+
+ clone(remoteUrl) {
+ return this.transitionTo('Cloning', remoteUrl);
+ }
+
+ destroy() {
+ this.transitionTo('Destroyed');
+ }
+}
+
+State.register(Empty);
diff --git a/lib/models/repository-states/index.js b/lib/models/repository-states/index.js
new file mode 100644
index 0000000000..e57e503eac
--- /dev/null
+++ b/lib/models/repository-states/index.js
@@ -0,0 +1,10 @@
+export {expectedDelegates} from './state';
+
+export {default as InitialState} from './loading';
+
+// Load and register remaining states
+import './empty';
+import './initializing';
+import './cloning';
+import './present';
+import './destroyed';
diff --git a/lib/models/repository-states/initializing.js b/lib/models/repository-states/initializing.js
new file mode 100644
index 0000000000..f5eb107167
--- /dev/null
+++ b/lib/models/repository-states/initializing.js
@@ -0,0 +1,13 @@
+import State from './state';
+import Loading from './loading';
+
+export default class Initializing extends Loading {
+ async start() {
+ await this.git().init(this.workdir());
+
+ // See https://github.com/babel/babel/issues/3930
+ await Loading.prototype.start.call(this);
+ }
+}
+
+State.register(Initializing);
diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js
new file mode 100644
index 0000000000..4214c6e96d
--- /dev/null
+++ b/lib/models/repository-states/loading.js
@@ -0,0 +1,32 @@
+import State from './state';
+
+export default class Loading extends State {
+ async start() {
+ if (await this.git().isGitRepository()) {
+ const history = await this.loadHistoryPayload();
+ this.transitionTo('Present', history);
+ } else {
+ this.transitionTo('Empty');
+ }
+ }
+
+ isLoading() {
+ return true;
+ }
+
+ async init() {
+ await this.getLoadPromise();
+ await this.repository.init();
+ }
+
+ async clone(remoteUrl) {
+ await this.getLoadPromise();
+ await this.repository.clone(remoteUrl);
+ }
+
+ destroy() {
+ this.transitionTo('Destroyed');
+ }
+}
+
+State.register(Loading);
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
new file mode 100644
index 0000000000..4385a6d111
--- /dev/null
+++ b/lib/models/repository-states/present.js
@@ -0,0 +1,426 @@
+import path from 'path';
+
+import State from './state';
+
+import {deleteFileOrFolder} from '../../helpers';
+import FilePatch from '../file-patch';
+import Hunk from '../hunk';
+import HunkLine from '../hunk-line';
+import DiscardHistory from '../discard-history';
+
+class Cache {
+ constructor() {
+ this.storage = new Map();
+ }
+
+ getOrSet(key, operation) {
+ const existing = this.storage.get(key);
+ if (existing !== undefined) {
+ return existing;
+ }
+
+ const created = operation();
+ this.storage.set(key, created);
+ return created;
+ }
+
+ clear() {
+ this.storage.clear();
+ }
+}
+
+export default class Present extends State {
+ constructor(repository, history) {
+ super(repository);
+
+ this.cache = new Cache();
+
+ this.discardHistory = new DiscardHistory(
+ this.createBlob.bind(this),
+ this.expandBlobToFile.bind(this),
+ this.mergeFile.bind(this),
+ this.workdir.bind(this),
+ {maxHistoryLength: 60},
+ );
+ }
+
+ isPresent() {
+ return true;
+ }
+
+ destroy() {
+ this.transitionTo('Destroyed');
+ }
+
+ refresh() {
+ this.cache.clear();
+ this.didUpdate();
+ }
+
+ // Git operations ////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ // Staging and unstaging
+
+ stageFiles(paths) {
+ return this.git().stageFiles(paths);
+ }
+
+ unstageFiles(paths) {
+ return this.git().unstageFiles(paths);
+ }
+
+ stageFilesFromParentCommit(paths) {
+ return this.git().unstageFiles(paths, 'HEAD~');
+ }
+
+ applyPatchToIndex(filePatch) {
+ const patchStr = filePatch.getHeaderString() + filePatch.toString();
+ return this.git().applyPatch(patchStr, {index: true});
+ }
+
+ applyPatchToWorkdir(filePatch) {
+ const patchStr = filePatch.getHeaderString() + filePatch.toString();
+ return this.git().applyPatch(patchStr);
+ }
+
+ // Committing
+
+ commit(message, options) {
+ return this.git().commit(formatCommitMessage(message), options);
+ }
+
+ // Merging
+
+ merge(branchName) {
+ return this.git().merge(branchName);
+ }
+
+ async abortMerge() {
+ await this.git().abortMerge();
+ this.refresh();
+ }
+
+ checkoutSide(side, paths) {
+ return this.git().checkoutSide(side, paths);
+ }
+
+ mergeFile(oursPath, commonBasePath, theirsPath, resultPath) {
+ return this.git().mergeFile(oursPath, commonBasePath, theirsPath, resultPath);
+ }
+
+ writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha) {
+ return this.git().writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha);
+ }
+
+ // Checkout
+
+ checkout(revision, options = {}) {
+ return this.git().checkout(revision, options);
+ }
+
+ checkoutPathsAtRevision(paths, revision = 'HEAD') {
+ return this.git().checkoutFiles(paths, revision);
+ }
+
+ // Remote interactions
+
+ fetch(branchName) {
+ return this.git().fetch(branchName);
+ }
+
+ pull(branchName) {
+ return this.git().pull(branchName);
+ }
+
+ push(branchName, options) {
+ return this.git().push(branchName, options);
+ }
+
+ // Configuration
+
+ setConfig(option, value, options) {
+ return this.git().setConfig(option, value, options);
+ }
+
+ // Direct blob interactions
+
+ createBlob(options) {
+ return this.git().createBlob(options);
+ }
+
+ expandBlobToFile(absFilePath, sha) {
+ return this.git().expandBlobToFile(absFilePath, sha);
+ }
+
+ // Discard history
+
+ createDiscardHistoryBlob() {
+ return this.discardHistory.createHistoryBlob();
+ }
+
+ async updateDiscardHistory() {
+ const history = await this.loadHistoryPayload();
+ this.discardHistory.updateHistory(history);
+ }
+
+ async storeBeforeAndAfterBlobs(filePaths, isSafe, destructiveAction, partialDiscardFilePath = null) {
+ const snapshots = await this.discardHistory.storeBeforeAndAfterBlobs(
+ filePaths,
+ isSafe,
+ destructiveAction,
+ partialDiscardFilePath,
+ );
+ if (snapshots) { await this.saveDiscardHistory(); }
+ return snapshots;
+ }
+
+ restoreLastDiscardInTempFiles(isSafe, partialDiscardFilePath = null) {
+ return this.discardHistory.restoreLastDiscardInTempFiles(isSafe, partialDiscardFilePath);
+ }
+
+ async popDiscardHistory(partialDiscardFilePath = null) {
+ const removed = await this.discardHistory.popHistory(partialDiscardFilePath);
+ if (removed) { await this.saveDiscardHistory(); }
+ }
+
+ clearDiscardHistory(partialDiscardFilePath = null) {
+ this.discardHistory.clearHistory(partialDiscardFilePath);
+ return this.saveDiscardHistory();
+ }
+
+ async discardWorkDirChangesForPaths(paths) {
+ const untrackedFiles = await this.git.getUntrackedFiles();
+ const [filesToRemove, filesToCheckout] = partition(paths, f => untrackedFiles.includes(f));
+ await this.git.checkoutFiles(filesToCheckout);
+ await Promise.all(filesToRemove.map(filePath => {
+ const absPath = path.join(this.getWorkingDirectoryPath(), filePath);
+ return deleteFileOrFolder(absPath);
+ }));
+ }
+
+ // Accessors /////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ // Index queries
+
+ getStatusesForChangedFiles() {
+ return this.cache.getOrSet('changed-files', () => {
+ return this.git().getStatusesForChangedFiles();
+ });
+ }
+
+ getStagedChangesSinceParentCommit() {
+ return this.cache.getOrSet('staged-changes-since-parent-commit', async () => {
+ try {
+ const stagedFiles = await this.git().diffFileStatus({staged: true, target: 'HEAD~'});
+ return Object.keys(stagedFiles).map(filePath => ({filePath, status: stagedFiles[filePath]}));
+ } catch (e) {
+ if (e.message.includes('ambiguous argument \'HEAD~\'')) {
+ return [];
+ } else {
+ throw e;
+ }
+ }
+ });
+ }
+
+ isPartiallyStaged(fileName) {
+ return this.cache.getOrSet(`is-partially-staged:${fileName}`, () => {
+ return this.git().isPartiallyStaged(fileName);
+ });
+ }
+
+ getFilePatchForPath(filePath, {staged, amending} = {staged: false, amending: false}) {
+ let desc = '';
+ if (staged && amending) {
+ desc = 'p';
+ } else if (staged) {
+ desc = 's';
+ } else {
+ desc = 'u';
+ }
+
+ return this.cache.getOrSet(`file-patch:${desc}:${filePath}`, async () => {
+ const options = {staged, amending};
+ if (amending) {
+ options.baseCommit = 'HEAD~';
+ }
+
+ const rawDiff = await this.git().getDiffForFilePath(filePath, options);
+ if (rawDiff) {
+ const [filePatch] = buildFilePatchesFromRawDiffs([rawDiff]);
+ return filePatch;
+ } else {
+ return null;
+ }
+ });
+ }
+
+ readFileFromIndex(filePath) {
+ return this.cache.getOrSet(`index:${filePath}`, () => {
+ return this.git().readFileFromIndex(filePath);
+ });
+ }
+
+ // Commit access
+
+ getLastCommit() {
+ return this.cache.getOrSet('last-commit', () => {
+ return this.git().getHeadCommit();
+ });
+ }
+
+ // Branches
+
+ getBranches() {
+ return this.cache.getOrSet('branches', () => {
+ return this.git().getBranches();
+ });
+ }
+
+ getCurrentBranch() {
+ return this.cache.getOrSet('current-branch', () => {
+ return this.git().getCurrentBranch();
+ });
+ }
+
+ // Merging and rebasing status
+
+ isMerging() {
+ return this.git().isMerging();
+ }
+
+ isRebasing() {
+ return this.git().isRebasing();
+ }
+
+ // Remotes
+
+ getRemotes() {
+ return this.cache.getOrSet('remotes', () => {
+ return this.git().getRemotes();
+ });
+ }
+
+ getRemoteForBranch(branchName) {
+ return this.cache.getOrSet(`remote-for-branch:${branchName}`, () => {
+ return this.git().getRemoteForBranch(branchName);
+ });
+ }
+
+ getAheadCount(branchName) {
+ return this.cache.getOrSet(`ahead-count:${branchName}`, () => {
+ return this.git().getAheadCount(branchName);
+ });
+ }
+
+ getBehindCount(branchName) {
+ return this.cache.getOrSet(`behind-count:${branchName}`, () => {
+ return this.git().getBehindCount(branchName);
+ });
+ }
+
+ getConfig(option, {local} = {local: false}) {
+ const desc = local ? 'l' : '';
+ return this.cache.getOrSet(`config:${desc}:${option}`, () => {
+ return this.git().getConfig(option, {local});
+ });
+ }
+
+ // Direct blob access
+
+ getBlobContents(sha) {
+ return this.cache.getOrSet(`blob:${sha}`, () => {
+ return this.git().getBlobContents(sha);
+ });
+ }
+
+ // Discard history
+
+ hasDiscardHistory(partialDiscardFilePath = null) {
+ return this.discardHistory.hasHistory(partialDiscardFilePath);
+ }
+
+ getDiscardHistory(partialDiscardFilePath = null) {
+ return this.discardHistory.getHistory(partialDiscardFilePath);
+ }
+
+ getLastHistorySnapshots(partialDiscardFilePath = null) {
+ return this.discardHistory.getLastSnapshots(partialDiscardFilePath);
+ }
+}
+
+State.register(Present);
+
+function partition(array, predicate) {
+ const matches = [];
+ const nonmatches = [];
+ array.forEach(item => {
+ if (predicate(item)) {
+ matches.push(item);
+ } else {
+ nonmatches.push(item);
+ }
+ });
+ return [matches, nonmatches];
+}
+
+function formatCommitMessage(message) {
+ // strip out comments
+ const messageWithoutComments = message.replace(/^#.*$/mg, '').trim();
+
+ // hard wrap message (except for first line) at 72 characters
+ let results = [];
+ messageWithoutComments.split('\n').forEach((line, index) => {
+ if (line.length <= 72 || index === 0) {
+ results.push(line);
+ } else {
+ const matches = line.match(/.{1,72}(\s|$)|\S+?(\s|$)/g)
+ .map(match => {
+ return match.endsWith('\n') ? match.substr(0, match.length - 1) : match;
+ });
+ results = results.concat(matches);
+ }
+ });
+
+ return results.join('\n');
+}
+
+function buildFilePatchesFromRawDiffs(rawDiffs) {
+ let diffLineNumber = 0;
+ return rawDiffs.map(patch => {
+ const hunks = patch.hunks.map(hunk => {
+ let oldLineNumber = hunk.oldStartLine;
+ let newLineNumber = hunk.newStartLine;
+ const hunkLines = hunk.lines.map(line => {
+ const status = HunkLine.statusMap[line[0]];
+ const text = line.slice(1);
+ let hunkLine;
+ if (status === 'unchanged') {
+ hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber, diffLineNumber++);
+ oldLineNumber++;
+ newLineNumber++;
+ } else if (status === 'added') {
+ hunkLine = new HunkLine(text, status, -1, newLineNumber, diffLineNumber++);
+ newLineNumber++;
+ } else if (status === 'deleted') {
+ hunkLine = new HunkLine(text, status, oldLineNumber, -1, diffLineNumber++);
+ oldLineNumber++;
+ } else if (status === 'nonewline') {
+ hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++);
+ } else {
+ throw new Error(`unknow status type: ${status}`);
+ }
+ return hunkLine;
+ });
+ return new Hunk(
+ hunk.oldStartLine,
+ hunk.newStartLine,
+ hunk.oldLineCount,
+ hunk.newLineCount,
+ hunk.heading,
+ hunkLines,
+ );
+ });
+ return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks);
+ });
+}
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
new file mode 100644
index 0000000000..026da1b8b7
--- /dev/null
+++ b/lib/models/repository-states/state.js
@@ -0,0 +1,430 @@
+import {nullCommit} from '../commit';
+import {nullBranch} from '../branch';
+import {nullRemote} from '../remote';
+
+export const expectedDelegates = [];
+
+/**
+ * Map of registered subclasses to allow states to transition to one another without circular dependencies.
+ * Subclasses of State should call `State.register` to add themselves here.
+ */
+const stateConstructors = new Map();
+
+/**
+ * Methods marked with this decorator on State should be delegated from a Repository to its current state. This will
+ * be verified by a unit test in `repository.test.js`.
+ */
+function shouldDelegate(target, name, descriptor) {
+ expectedDelegates.push(name);
+ return descriptor;
+}
+
+/**
+ * Base class for Repository states. Implements default "null" behavior.
+ */
+export default class State {
+ constructor(repository) {
+ this.repository = repository;
+ }
+
+ static register(Subclass) {
+ stateConstructors.set(Subclass.name, Subclass);
+ }
+
+ // This state has just been entered. Perform any asynchronous initialization that needs to occur.
+ start() {
+ return Promise.resolve();
+ }
+
+ // State probe predicates ////////////////////////////////////////////////////////////////////////////////////////////
+ // Allow external callers to identify which state a Repository is in if necessary.
+
+ @shouldDelegate
+ isLoading() {
+ return false;
+ }
+
+ @shouldDelegate
+ isEmpty() {
+ return false;
+ }
+
+ @shouldDelegate
+ isPresent() {
+ return false;
+ }
+
+ @shouldDelegate
+ isDestroyed() {
+ return false;
+ }
+
+ // Lifecycle actions /////////////////////////////////////////////////////////////////////////////////////////////////
+ // These generally default to rejecting a Promise with an error.
+
+ @shouldDelegate
+ init() {
+ return unsupportedOperationPromise(this, 'init');
+ }
+
+ @shouldDelegate
+ clone(remoteUrl) {
+ return unsupportedOperationPromise(this, 'clone');
+ }
+
+ @shouldDelegate
+ destroy() {
+ return unsupportedOperation(this, 'destroy');
+ }
+
+ @shouldDelegate
+ refresh() {
+ // No-op
+ }
+
+ // Git operations ////////////////////////////////////////////////////////////////////////////////////////////////////
+ // These default to rejecting a Promise with an error stating that the operation is not supported in the current
+ // state.
+
+ // Staging and unstaging
+
+ @shouldDelegate
+ stageFiles(paths) {
+ return unsupportedOperationPromise(this, 'stageFiles');
+ }
+
+ @shouldDelegate
+ unstageFiles(paths) {
+ return unsupportedOperationPromise(this, 'unstageFiles');
+ }
+
+ @shouldDelegate
+ stageFilesFromParentCommit(paths) {
+ return unsupportedOperationPromise(this, 'stageFilesFromParentCommit');
+ }
+
+ @shouldDelegate
+ applyPatchToIndex(patch) {
+ return unsupportedOperationPromise(this, 'applyPatchToIndex');
+ }
+
+ @shouldDelegate
+ applyPatchToWorkdir(patch) {
+ return unsupportedOperationPromise(this, 'applyPatchToWorkdir');
+ }
+
+ // Committing
+
+ @shouldDelegate
+ commit(message, options) {
+ return unsupportedOperationPromise(this, 'commit');
+ }
+
+ // Merging
+
+ @shouldDelegate
+ merge(branchName) {
+ return unsupportedOperationPromise(this, 'merge');
+ }
+
+ @shouldDelegate
+ abortMerge() {
+ return unsupportedOperationPromise(this, 'abortMerge');
+ }
+
+ @shouldDelegate
+ checkoutSide(side, paths) {
+ return unsupportedOperationPromise(this, 'checkoutSide');
+ }
+
+ @shouldDelegate
+ mergeFile(oursPath, commonBasePath, theirsPath, resultPath) {
+ return unsupportedOperationPromise(this, 'mergeFile');
+ }
+
+ @shouldDelegate
+ writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha) {
+ return unsupportedOperationPromise(this, 'writeMergeConflictToIndex');
+ }
+
+ // Checkout
+
+ @shouldDelegate
+ checkout(revision, options = {}) {
+ return unsupportedOperationPromise(this, 'checkout');
+ }
+
+ @shouldDelegate
+ checkoutPathsAtRevision(paths, revision = 'HEAD') {
+ return unsupportedOperationPromise(this, 'checkoutPathsAtRevision');
+ }
+
+ // Remote interactions
+
+ @shouldDelegate
+ fetch(branchName) {
+ return unsupportedOperationPromise(this, 'fetch');
+ }
+
+ @shouldDelegate
+ pull(branchName) {
+ return unsupportedOperationPromise(this, 'pull');
+ }
+
+ @shouldDelegate
+ push(branchName) {
+ return unsupportedOperationPromise(this, 'push');
+ }
+
+ // Configuration
+
+ @shouldDelegate
+ setConfig(option, value, {replaceAll} = {}) {
+ return unsupportedOperationPromise(this, 'setConfig');
+ }
+
+ // Direct blob interactions
+
+ @shouldDelegate
+ createBlob({filePath, stdin} = {}) {
+ return unsupportedOperationPromise(this, 'createBlob');
+ }
+
+ @shouldDelegate
+ expandBlobToFile(absFilePath, sha) {
+ return unsupportedOperationPromise(this, 'expandBlobToFile');
+ }
+
+ // Discard history
+
+ @shouldDelegate
+ createDiscardHistoryBlob() {
+ return unsupportedOperationPromise(this, 'createDiscardHistoryBlob');
+ }
+
+ @shouldDelegate
+ saveDiscardHistory() {
+ return unsupportedOperationPromise(this, 'saveDiscardHistory');
+ }
+
+ @shouldDelegate
+ updateDiscardHistory() {
+ return unsupportedOperationPromise(this, 'updateDiscardHistory');
+ }
+
+ @shouldDelegate
+ storeBeforeAndAfterBlobs(filePaths, isSafe, destructiveAction, partialDiscardFilePath = null) {
+ return unsupportedOperationPromise(this, 'storeBeforeAndAfterBlobs');
+ }
+
+ @shouldDelegate
+ restoreLastDiscardInTempFiles(isSafe, partialDiscardFilePath = null) {
+ return unsupportedOperationPromise(this, 'restoreLastDiscardInTempFiles');
+ }
+
+ @shouldDelegate
+ popDiscardHistory(partialDiscardFilePath = null) {
+ return unsupportedOperationPromise(this, 'popDiscardHistory');
+ }
+
+ @shouldDelegate
+ clearDiscardHistory(partialDiscardFilePath = null) {
+ return unsupportedOperationPromise(this, 'clearDiscardHistory');
+ }
+
+ @shouldDelegate
+ discardWorkDirChangesForPaths(paths) {
+ return unsupportedOperationPromise(this, 'discardWorkDirChangesForPaths');
+ }
+
+ // Accessors /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // When possible, these default to "empty" results when invoked in states that don't have information available, or
+ // fail in a way that's consistent with the requested information not being found.
+
+ // Index queries
+
+ @shouldDelegate
+ getStatusesForChangedFiles() {
+ return Promise.resolve({
+ stagedFiles: [],
+ unstagedFiles: [],
+ mergeConflictFiles: [],
+ });
+ }
+
+ @shouldDelegate
+ getStagedChangesSinceParentCommit() {
+ return Promise.resolve([]);
+ }
+
+ @shouldDelegate
+ isPartiallyStaged(fileName) {
+ return Promise.resolve(false);
+ }
+
+ @shouldDelegate
+ getFilePatchForPath(filePath, options = {}) {
+ return Promise.resolve(null);
+ }
+
+ @shouldDelegate
+ readFileFromIndex(filePath) {
+ return Promise.reject(new Error(`fatal: Path ${filePath} does not exist (neither on disk nor in the index).`));
+ }
+
+ // Commit access
+
+ @shouldDelegate
+ getLastCommit() {
+ return Promise.resolve(nullCommit);
+ }
+
+ // Branches
+
+ @shouldDelegate
+ getBranches() {
+ return Promise.resolve([]);
+ }
+
+ @shouldDelegate
+ getCurrentBranch() {
+ return Promise.resolve(nullBranch);
+ }
+
+ // Merging and rebasing status
+
+ @shouldDelegate
+ isMerging() {
+ return Promise.resolve(false);
+ }
+
+ @shouldDelegate
+ isRebasing() {
+ return Promise.resolve(false);
+ }
+
+ // Remotes
+
+ @shouldDelegate
+ getRemotes() {
+ return Promise.resolve([]);
+ }
+
+ @shouldDelegate
+ getRemoteForBranch(branchName) {
+ return Promise.resolve(nullRemote);
+ }
+
+ @shouldDelegate
+ getAheadCount(branchName) {
+ return Promise.resolve(null);
+ }
+
+ @shouldDelegate
+ getBehindCount(branchName) {
+ return Promise.resolve(null);
+ }
+
+ @shouldDelegate
+ getConfig(option, {local} = {}) {
+ return Promise.resolve(null);
+ }
+
+ // Direct blob access
+
+ @shouldDelegate
+ getBlobContents(sha) {
+ return Promise.reject(new Error(`fatal: Not a valid object name ${sha}`));
+ }
+
+ // Discard history
+
+ @shouldDelegate
+ hasDiscardHistory(partialDiscardFilePath = null) {
+ return false;
+ }
+
+ @shouldDelegate
+ getDiscardHistory(partialDiscardFilePath = null) {
+ return [];
+ }
+
+ @shouldDelegate
+ getLastHistorySnapshots(partialDiscardFilePath = null) {
+ return null;
+ }
+
+ // Internal //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Non-delegated methods that provide subclasses with convenient access to Repository properties.
+
+ git() {
+ return this.repository.git;
+ }
+
+ workdir() {
+ return this.repository.getWorkingDirectoryPath();
+ }
+
+ // Return a Promise that will resolve once the state transitions from Loading.
+ getLoadPromise() {
+ return this.repository.getLoadPromise();
+ }
+
+ // Initiate a transition to another state.
+ transitionTo(stateName, ...payload) {
+ const StateConstructor = stateConstructors.get(stateName);
+ if (StateConstructor === undefined) {
+ throw new Error(`Attempt to transition to unrecognized state ${stateName}`);
+ }
+ return this.repository.transition(this, StateConstructor, ...payload);
+ }
+
+ // Event broadcast
+
+ didDestroy() {
+ return this.repository.emitter.emit('did-destroy');
+ }
+
+ didUpdate() {
+ return this.repository.emitter.emit('did-update');
+ }
+
+ // Parse a DiscardHistory payload from the SHA recorded in config.
+ async loadHistoryPayload() {
+ const historySha = await this.git().getConfig('atomGithub.historySha');
+ if (!historySha) {
+ return {};
+ }
+
+ let blob;
+ try {
+ blob = await this.getBlobContents(historySha);
+ } catch (e) {
+ if (/Not a valid object name/.test(e.stdErr)) {
+ return {};
+ }
+
+ throw e;
+ }
+
+ try {
+ return JSON.parse(blob);
+ } catch (e) {
+ return {};
+ }
+ }
+
+ // Debugging assistance.
+
+ toString() {
+ return this.constructor.name;
+ }
+}
+
+function unsupportedOperation(self, opName) {
+ throw new Error(`${opName} is not available in ${self} state`);
+}
+
+function unsupportedOperationPromise(self, opName) {
+ return Promise.reject(new Error(`${opName} is not available in ${self} state`));
+}
From 589192c19a16703408420af7699f318cff25acb9 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:37:58 -0400
Subject: [PATCH 062/161] fetch, push, and pull accept a remote name argument
---
lib/git-shell-out-strategy.js | 9 +++------
test/git-strategies.test.js | 10 +++++-----
2 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js
index 213ba09e71..699e1aeea3 100644
--- a/lib/git-shell-out-strategy.js
+++ b/lib/git-shell-out-strategy.js
@@ -472,18 +472,15 @@ export default class GitShellOutStrategy {
}
}
- async fetch(branchName) {
- const remote = await this.getRemoteForBranch(branchName);
+ fetch(remote, branchName) {
return this.exec(['fetch', remote, branchName], {useGitPromptServer: true, writeOperation: true});
}
- async pull(branchName) {
- const remote = await this.getRemoteForBranch(branchName);
+ pull(remote, branchName) {
return this.gpgExec(['pull', remote, branchName], {useGitPromptServer: true, writeOperation: true});
}
- async push(branchName, options = {}) {
- const remote = await this.getRemoteForBranch(branchName);
+ push(remote, branchName, options = {}) {
const args = ['push', remote || 'origin', branchName];
if (options.setUpstream) { args.push('--set-upstream'); }
if (options.force) { args.push('--force'); }
diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js
index a577956183..f876811199 100644
--- a/test/git-strategies.test.js
+++ b/test/git-strategies.test.js
@@ -468,7 +468,7 @@ import {fsStat} from '../lib/helpers';
await git.exec(['config', 'push.default', 'upstream']);
assert.equal(await git.getBehindCount('master'), 0);
assert.equal(await git.getAheadCount('master'), 0);
- await git.fetch('master');
+ await git.fetch('origin', 'master');
assert.equal(await git.getBehindCount('master'), 1);
assert.equal(await git.getAheadCount('master'), 0);
await git.commit('new commit', {allowEmpty: true});
@@ -646,7 +646,7 @@ import {fsStat} from '../lib/helpers';
command: 'pull',
progressiveTense: 'pulling',
usesPromptServerAlready: true,
- action: () => git.pull('some-branch'),
+ action: () => git.pull('origin', 'some-branch'),
},
];
@@ -719,17 +719,17 @@ import {fsStat} from '../lib/helpers';
{
command: 'fetch',
progressiveTense: 'fetching',
- action: () => git.fetch('some-branch'),
+ action: () => git.fetch('origin', 'some-branch'),
},
{
command: 'pull',
progressiveTense: 'pulling',
- action: () => git.pull('some-branch'),
+ action: () => git.pull('origin', 'some-branch'),
},
{
command: 'push',
progressiveTense: 'pushing',
- action: () => git.push('some-branch'),
+ action: () => git.push('origin', 'some-branch'),
},
];
From 6b5f83220c059aadac8cf4923caca67088b7a89c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:38:46 -0400
Subject: [PATCH 063/161] Move getRemoteForBranchName() into Repository
---
lib/git-shell-out-strategy.js | 9 ---------
lib/models/repository.js | 12 ++++++++++--
test/git-strategies.test.js | 15 ---------------
3 files changed, 10 insertions(+), 26 deletions(-)
diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js
index 699e1aeea3..759b5e6005 100644
--- a/lib/git-shell-out-strategy.js
+++ b/lib/git-shell-out-strategy.js
@@ -463,15 +463,6 @@ export default class GitShellOutStrategy {
return this.exec(args, {writeOperation: true});
}
- async getRemoteForBranch(branchName) {
- try {
- const output = await this.exec(['config', `branch.${branchName}.remote`]);
- return output.trim();
- } catch (e) {
- return null;
- }
- }
-
fetch(remote, branchName) {
return this.exec(['fetch', remote, branchName], {useGitPromptServer: true, writeOperation: true});
}
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 5eb8ff21b5..2e5ec01bf8 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -4,7 +4,7 @@ import {Emitter} from 'event-kit';
import CompositeGitStrategy from '../composite-git-strategy';
import {readFile} from '../helpers';
-
+import Remote, {nullRemote} from './remote';
import {InitialState} from './repository-states';
const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
@@ -159,6 +159,15 @@ 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);
+ }
+ }
+
async saveDiscardHistory() {
const historySha = await this.createDiscardHistoryBlob();
await this.setConfig('atomGithub.historySha', historySha);
@@ -229,7 +238,6 @@ const delegates = [
'isRebasing',
'getRemotes',
- 'getRemoteForBranch',
'getAheadCount',
'getBehindCount',
diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js
index f876811199..3b2022e7c5 100644
--- a/test/git-strategies.test.js
+++ b/test/git-strategies.test.js
@@ -542,18 +542,6 @@ import {fsStat} from '../lib/helpers';
});
});
- describe('getRemoteForBranch(branchName)', function() {
- it('returns the name of the remote associated with the branch, and null if none exists', async function() {
- const workingDirPath = await cloneRepository('three-files');
- const git = createTestStrategy(workingDirPath);
- assert.equal(await git.getRemoteForBranch('master'), 'origin');
- await git.exec(['remote', 'rename', 'origin', 'foo']);
- assert.equal(await git.getRemoteForBranch('master'), 'foo');
- await git.exec(['remote', 'rm', 'foo']);
- assert.isNull(await git.getRemoteForBranch('master'));
- });
- });
-
describe('getBranches()', function() {
it('returns an array of all branches', async function() {
const workingDirPath = await cloneRepository('three-files');
@@ -626,7 +614,6 @@ import {fsStat} from '../lib/helpers';
beforeEach(async function() {
const workingDirPath = await cloneRepository('multiple-commits');
git = createTestStrategy(workingDirPath);
- sinon.stub(git, 'getRemoteForBranch').returns(Promise.resolve('origin'));
});
const operations = [
@@ -701,8 +688,6 @@ import {fsStat} from '../lib/helpers';
prompt: Promise.resolve(''),
});
- sinon.stub(git, 'getRemoteForBranch').returns(Promise.resolve('origin'));
-
originalEnv = {};
['PATH', 'DISPLAY', 'GIT_ASKPASS', 'SSH_ASKPASS', 'GIT_SSH_COMMAND'].forEach(varName => {
originalEnv[varName] = process.env[varName];
From 56d277e0d313920575a85c1faba59d2d2c17522e Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:42:51 -0400
Subject: [PATCH 064/161] Remote.getNameOr() lets you specify a default
---
lib/models/remote.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/lib/models/remote.js b/lib/models/remote.js
index 05d237faa9..e0c843fc5f 100644
--- a/lib/models/remote.js
+++ b/lib/models/remote.js
@@ -7,6 +7,10 @@ export default class Remote {
return this.name;
}
+ getNameOr(fallback) {
+ return this.getName();
+ }
+
isPresent() {
return true;
}
@@ -17,6 +21,10 @@ export const nullRemote = {
return '';
},
+ getNameOr(fallback) {
+ return fallback;
+ },
+
isPresent() {
return false;
},
From aff481db32dd5703d09bf9827a6e6ac94fbc0189 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:43:08 -0400
Subject: [PATCH 065/161] saveDiscardHistory() is a native Repository method
---
lib/models/repository.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 2e5ec01bf8..c37ec4f7bc 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -215,7 +215,6 @@ const delegates = [
'expandBlobToFile',
'createDiscardHistoryBlob',
- 'saveDiscardHistory',
'updateDiscardHistory',
'storeBeforeAndAfterBlobs',
'restoreLastDiscardInTempFiles',
From d1eb89f4252c9274b4cda0957fa4b752a04c5c2a Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:44:29 -0400
Subject: [PATCH 066/161] :fire: methods that should no longer be delegated
---
lib/models/repository-states/state.js | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index 026da1b8b7..e7969c3416 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -1,6 +1,5 @@
import {nullCommit} from '../commit';
import {nullBranch} from '../branch';
-import {nullRemote} from '../remote';
export const expectedDelegates = [];
@@ -202,11 +201,6 @@ export default class State {
return unsupportedOperationPromise(this, 'createDiscardHistoryBlob');
}
- @shouldDelegate
- saveDiscardHistory() {
- return unsupportedOperationPromise(this, 'saveDiscardHistory');
- }
-
@shouldDelegate
updateDiscardHistory() {
return unsupportedOperationPromise(this, 'updateDiscardHistory');
@@ -310,11 +304,6 @@ export default class State {
return Promise.resolve([]);
}
- @shouldDelegate
- getRemoteForBranch(branchName) {
- return Promise.resolve(nullRemote);
- }
-
@shouldDelegate
getAheadCount(branchName) {
return Promise.resolve(null);
@@ -370,6 +359,14 @@ export default class State {
return this.repository.getLoadPromise();
}
+ getRemoteForBranch(branchName) {
+ return this.repository.getRemoteForBranch(branchName);
+ }
+
+ saveDiscardHistory() {
+ return this.repository.saveDiscardHistory();
+ }
+
// Initiate a transition to another state.
transitionTo(stateName, ...payload) {
const StateConstructor = stateConstructors.get(stateName);
From 70bebc347a820942d8da90a7b6b98f1d70d51005 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:45:14 -0400
Subject: [PATCH 067/161] Use getBlobContents() from git strategy from
loadHistoryPayload()
---
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 e7969c3416..e0dd1afc30 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -395,7 +395,7 @@ export default class State {
let blob;
try {
- blob = await this.getBlobContents(historySha);
+ blob = await this.git().getBlobContents(historySha);
} catch (e) {
if (/Not a valid object name/.test(e.stdErr)) {
return {};
From 1332c64442de6f808e2a362ac7cc0b89345faf42 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:45:31 -0400
Subject: [PATCH 068/161] Cache invalidation with a fancy decorator
---
lib/models/repository-states/present.js | 94 +++++++++++++++++++------
1 file changed, 74 insertions(+), 20 deletions(-)
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index 4385a6d111..c22f1cf937 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -24,11 +24,40 @@ class Cache {
return created;
}
+ invalidate(...keys) {
+ for (let i = 0; i < keys.length; i++) {
+ this.storage.delete(keys[i]);
+ }
+ }
+
clear() {
this.storage.clear();
}
}
+/**
+ * Decorator for an async method that invalidates the cache after execution (regardless of success or failure.)
+ */
+function invalidate() {
+ return function(target, name, descriptor) {
+ const original = descriptor.value;
+ descriptor.value = function(...args) {
+ const cache = this.cache;
+
+ return original.apply(this, args).then(
+ result => {
+ cache.clear();
+ return result;
+ },
+ err => {
+ return Promise.reject(err);
+ },
+ );
+ };
+ return descriptor;
+ };
+}
+
export default class Present extends State {
constructor(repository, history) {
super(repository);
@@ -61,23 +90,28 @@ export default class Present extends State {
// Staging and unstaging
+ @invalidate()
stageFiles(paths) {
return this.git().stageFiles(paths);
}
+ @invalidate()
unstageFiles(paths) {
return this.git().unstageFiles(paths);
}
+ @invalidate()
stageFilesFromParentCommit(paths) {
return this.git().unstageFiles(paths, 'HEAD~');
}
+ @invalidate()
applyPatchToIndex(filePatch) {
const patchStr = filePatch.getHeaderString() + filePatch.toString();
return this.git().applyPatch(patchStr, {index: true});
}
+ @invalidate()
applyPatchToWorkdir(filePatch) {
const patchStr = filePatch.getHeaderString() + filePatch.toString();
return this.git().applyPatch(patchStr);
@@ -85,65 +119,86 @@ export default class Present extends State {
// Committing
+ @invalidate()
commit(message, options) {
return this.git().commit(formatCommitMessage(message), options);
}
// Merging
+ @invalidate()
merge(branchName) {
return this.git().merge(branchName);
}
- async abortMerge() {
- await this.git().abortMerge();
- this.refresh();
+ @invalidate()
+ abortMerge() {
+ return this.git().abortMerge();
}
+ @invalidate()
checkoutSide(side, paths) {
return this.git().checkoutSide(side, paths);
}
+ @invalidate()
mergeFile(oursPath, commonBasePath, theirsPath, resultPath) {
return this.git().mergeFile(oursPath, commonBasePath, theirsPath, resultPath);
}
+ @invalidate()
writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha) {
return this.git().writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha);
}
// Checkout
+ @invalidate()
checkout(revision, options = {}) {
return this.git().checkout(revision, options);
}
+ @invalidate()
checkoutPathsAtRevision(paths, revision = 'HEAD') {
return this.git().checkoutFiles(paths, revision);
}
// Remote interactions
- fetch(branchName) {
- return this.git().fetch(branchName);
+ @invalidate()
+ async fetch(branchName) {
+ const remote = await this.getRemoteForBranch(branchName);
+ if (!remote.isPresent()) {
+ return;
+ }
+ await this.git().fetch(remote.getName(), branchName);
}
- pull(branchName) {
- return this.git().pull(branchName);
+ @invalidate()
+ async pull(branchName) {
+ const remote = await this.getRemoteForBranch(branchName);
+ if (!remote.isPresent()) {
+ return;
+ }
+ await this.git().pull(remote.getName(), branchName);
}
- push(branchName, options) {
- return this.git().push(branchName, options);
+ @invalidate()
+ async push(branchName, options) {
+ const remote = await this.getRemoteForBranch(branchName);
+ return this.git().push(remote.getNameOr('origin'), branchName, options);
}
// Configuration
+ @invalidate()
setConfig(option, value, options) {
return this.git().setConfig(option, value, options);
}
// Direct blob interactions
+ @invalidate()
createBlob(options) {
return this.git().createBlob(options);
}
@@ -170,7 +225,9 @@ export default class Present extends State {
destructiveAction,
partialDiscardFilePath,
);
- if (snapshots) { await this.saveDiscardHistory(); }
+ if (snapshots) {
+ await this.saveDiscardHistory();
+ }
return snapshots;
}
@@ -180,7 +237,9 @@ export default class Present extends State {
async popDiscardHistory(partialDiscardFilePath = null) {
const removed = await this.discardHistory.popHistory(partialDiscardFilePath);
- if (removed) { await this.saveDiscardHistory(); }
+ if (removed) {
+ await this.saveDiscardHistory();
+ }
}
clearDiscardHistory(partialDiscardFilePath = null) {
@@ -188,12 +247,13 @@ export default class Present extends State {
return this.saveDiscardHistory();
}
+ @invalidate()
async discardWorkDirChangesForPaths(paths) {
- const untrackedFiles = await this.git.getUntrackedFiles();
+ const untrackedFiles = await this.git().getUntrackedFiles();
const [filesToRemove, filesToCheckout] = partition(paths, f => untrackedFiles.includes(f));
- await this.git.checkoutFiles(filesToCheckout);
+ await this.git().checkoutFiles(filesToCheckout);
await Promise.all(filesToRemove.map(filePath => {
- const absPath = path.join(this.getWorkingDirectoryPath(), filePath);
+ const absPath = path.join(this.workdir(), filePath);
return deleteFileOrFolder(absPath);
}));
}
@@ -301,12 +361,6 @@ export default class Present extends State {
});
}
- getRemoteForBranch(branchName) {
- return this.cache.getOrSet(`remote-for-branch:${branchName}`, () => {
- return this.git().getRemoteForBranch(branchName);
- });
- }
-
getAheadCount(branchName) {
return this.cache.getOrSet(`ahead-count:${branchName}`, () => {
return this.git().getAheadCount(branchName);
From 979b815b8e1250cd85cf80681e139fa9ec64b2b3 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 13 Apr 2017 12:45:59 -0400
Subject: [PATCH 069/161] Refresh explicitly in tests
---
test/models/repository.test.js | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index c48e8bae8c..67bebe7efa 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -499,17 +499,27 @@ describe('Repository', function() {
});
describe('getRemoteForBranch(branchName)', function() {
- it('returns the remote name associated to the supplied branch name, null if none exists', async function() {
+ it('returns the remote associated to the supplied branch name', async function() {
const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true});
const localRepo = new Repository(localRepoPath);
await localRepo.getLoadPromise();
- assert.equal(await localRepo.getRemoteForBranch('master'), 'origin');
+ const remote0 = await localRepo.getRemoteForBranch('master');
+ assert.isTrue(remote0.isPresent());
+ assert.equal(remote0.getName(), 'origin');
+
await localRepo.git.exec(['remote', 'rename', 'origin', 'foo']);
- assert.equal(await localRepo.getRemoteForBranch('master'), 'foo');
+ localRepo.refresh();
+
+ const remote1 = await localRepo.getRemoteForBranch('master');
+ assert.isTrue(remote1.isPresent());
+ assert.equal(remote1.getName(), 'foo');
await localRepo.git.exec(['remote', 'rm', 'foo']);
- assert.isNull(await localRepo.getRemoteForBranch('master'));
+ localRepo.refresh();
+
+ const remote2 = await localRepo.getRemoteForBranch('master');
+ assert.isFalse(remote2.isPresent());
});
});
@@ -719,8 +729,8 @@ describe('Repository', function() {
assert.equal(await repo.isMerging(), true);
try {
- await assert.isRejected(repo.abortMerge(), /^git merge --abort/);
- assert(false);
+ await repo.abortMerge();
+ assert.fail(null, null, 'repo.abortMerge() unexepctedly succeeded');
} catch (e) {
assert.match(e.command, /^git merge --abort/);
}
From 0d49c457926aaea39eb116c07876795dbaca70d9 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:34:26 -0400
Subject: [PATCH 070/161] Model detached Branches
---
lib/models/branch.js | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/lib/models/branch.js b/lib/models/branch.js
index 9047086b5b..3d2aecd3ea 100644
--- a/lib/models/branch.js
+++ b/lib/models/branch.js
@@ -1,12 +1,23 @@
+const DETACHED = {};
+
export default class Branch {
- constructor(name) {
+ constructor(name, detached = null) {
this.name = name;
+ this.isDetached = detached === DETACHED;
+ }
+
+ static createDetached(describe) {
+ return new Branch(describe, DETACHED);
}
getName() {
return this.name;
}
+ isDetached() {
+ return this.isDetached;
+ }
+
isPresent() {
return true;
}
@@ -17,6 +28,10 @@ export const nullBranch = {
return '';
},
+ isDetached() {
+ return false;
+ },
+
isPresent() {
return false;
},
From 5e07c29c94c634c4feaf6830f60661490ad8fa7d Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:35:35 -0400
Subject: [PATCH 071/161] Move URL extraction to Remote
---
lib/models/remote.js | 43 +++++++++++++++++++++++++++++++++-
lib/models/repository.js | 19 ---------------
test/models/remote.test.js | 41 ++++++++++++++++++++++++++++++++
test/models/repository.test.js | 32 -------------------------
4 files changed, 83 insertions(+), 52 deletions(-)
create mode 100644 test/models/remote.test.js
diff --git a/lib/models/remote.js b/lib/models/remote.js
index e0c843fc5f..b802449e88 100644
--- a/lib/models/remote.js
+++ b/lib/models/remote.js
@@ -1,12 +1,34 @@
export default class Remote {
- constructor(name) {
+ constructor(name, url) {
this.name = name;
+ this.url = url;
+
+ const {isGithubRepo, owner, repo} = githubInfoFromRemote(url);
+ this.githubRepo = isGithubRepo;
+ this.owner = owner;
+ this.repo = repo;
}
getName() {
return this.name;
}
+ getUrl() {
+ return this.url;
+ }
+
+ isGithubRepo() {
+ return this.githubRepo;
+ }
+
+ getOwner() {
+ return this.owner;
+ }
+
+ getRepo() {
+ return this.repo;
+ }
+
getNameOr(fallback) {
return this.getName();
}
@@ -16,6 +38,25 @@ export default class Remote {
}
}
+function githubInfoFromRemote(remoteUrl) {
+ // proto login domain owner repo
+ const regex = /(?:.+:\/\/)?(?:.+@)?github\.com[:/]([^/]+)\/(.+)/;
+ const match = remoteUrl.match(regex);
+ if (match) {
+ return {
+ isGithubRepo: true,
+ owner: match[1],
+ repo: match[2].replace(/\.git$/, ''),
+ };
+ } else {
+ return {
+ isGithubRepo: false,
+ owner: null,
+ repo: null,
+ };
+ }
+}
+
export const nullRemote = {
getName() {
return '';
diff --git a/lib/models/repository.js b/lib/models/repository.js
index c37ec4f7bc..1e228cc667 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -29,25 +29,6 @@ export class CommitError extends Error {
}
export default class Repository {
- static githubInfoFromRemote(remote) {
- // proto login domain owner repo
- const regex = /(?:.+:\/\/)?(?:.+@)?github\.com[:/]([^/]+)\/(.+)/;
- const match = remote.match(regex);
- if (match) {
- return {
- githubRepo: true,
- owner: match[1],
- name: match[2].replace(/\.git$/, ''),
- };
- } else {
- return {
- githubRepo: false,
- owner: null,
- name: null,
- };
- }
- }
-
constructor(workingDirectoryPath, gitStrategy = null) {
this.workingDirectoryPath = workingDirectoryPath;
this.git = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
diff --git a/test/models/remote.test.js b/test/models/remote.test.js
new file mode 100644
index 0000000000..88f794326d
--- /dev/null
+++ b/test/models/remote.test.js
@@ -0,0 +1,41 @@
+import Remote from '../../lib/models/remote';
+
+describe('Remote', function() {
+ it('detects and extracts information from GitHub repository URLs', function() {
+ const urls = [
+ 'git@github.com:atom/github.git',
+ 'https://github.com/atom/github.git',
+ 'https://git:pass@github.com/atom/github.git',
+ 'ssh+https://github.com/atom/github.git',
+ 'git://github.com/atom/github',
+ 'ssh://git@github.com:atom/github.git',
+ ];
+
+ for (const url of urls) {
+ const remote = new Remote('origin', url);
+
+ assert.equal(remote.getName(), 'origin');
+ assert.equal(remote.getUrl(), url);
+ assert.isTrue(remote.isGithubRepo());
+ assert.equal(remote.getOwner(), 'atom');
+ assert.equal(remote.getRepo(), 'github');
+ }
+ });
+
+ it('detects non-GitHub remotes', function() {
+ const urls = [
+ 'git@gitlab.com:atom/github.git',
+ 'atom/github',
+ ];
+
+ for (const url of urls) {
+ const remote = new Remote('origin', url);
+
+ assert.equal(remote.getName(), 'origin');
+ assert.equal(remote.getUrl(), url);
+ assert.isFalse(remote.isGithubRepo());
+ assert.isNull(remote.getOwner());
+ assert.isNull(remote.getRepo());
+ }
+ });
+});
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index 67bebe7efa..bf4a422052 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -27,38 +27,6 @@ describe('Repository', function() {
assert.lengthOf(missing, 0);
});
- describe('githubInfoFromRemote', function() {
- it('returns info about a GitHub repo based on the remote URL', function() {
- const atomRepo = {
- githubRepo: true,
- owner: 'atom',
- name: 'github',
- };
-
- const notARepo = {
- githubRepo: false,
- owner: null,
- name: null,
- };
-
- const remotes = [
- 'git@github.com:atom/github.git',
- 'https://github.com/atom/github.git',
- 'https://git:pass@github.com/atom/github.git',
- 'ssh+https://github.com/atom/github.git',
- 'git://github.com/atom/github',
- 'ssh://git@github.com:atom/github.git',
- ];
-
- for (const remote of remotes) {
- assert.deepEqual(Repository.githubInfoFromRemote(remote), atomRepo);
- }
-
- assert.deepEqual(Repository.githubInfoFromRemote('git@gitlab.com:atom/github.git'), notARepo);
- assert.deepEqual(Repository.githubInfoFromRemote('atom/github'), notARepo);
- });
- });
-
describe('init', function() {
it('creates a repository in the given dir and returns the repository', async function() {
const soonToBeRepositoryPath = fs.realpathSync(temp.mkdirSync());
From b193c76d6cd5b31284c00fdb78ae96da715d4515 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:35:58 -0400
Subject: [PATCH 072/161] Helper to get the SHA of a working directory
---
test/helpers.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/test/helpers.js b/test/helpers.js
index eb843d86dc..644182f7da 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -38,6 +38,12 @@ export async function cloneRepository(repoName = 'three-files') {
return copyCachedRepo(repoName);
}
+export async function sha(directory) {
+ const git = new GitShellOutStrategy(directory);
+ const head = await git.getHeadCommit();
+ return head.sha;
+}
+
/*
* Initialize an empty repository at a temporary path.
*/
From c3f3bae62cf93a6fbd5fe9d0c13c2ada714c9aff Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:36:20 -0400
Subject: [PATCH 073/161] Update buildRepository() for stateful Repositories
---
test/helpers.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/test/helpers.js b/test/helpers.js
index 644182f7da..8f5cf2dcfe 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -86,8 +86,10 @@ export async function getHeadCommitOnRemote(remotePath) {
return git.getHeadCommit();
}
-export function buildRepository(workingDirPath) {
- return Repository.open(workingDirPath);
+export async function buildRepository(workingDirPath) {
+ const repository = new Repository(workingDirPath);
+ await repository.getLoadPromise();
+ return repository;
}
export function assertDeepPropertyVals(actual, expected) {
From 258e57f45584b340b56f29abf59e5e95e5e8f5e9 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:37:02 -0400
Subject: [PATCH 074/161] Detect a Repository's state by name
---
lib/models/repository.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 1e228cc667..3cb3a22051 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -120,6 +120,10 @@ export default class Repository {
return path.join(this.getWorkingDirectoryPath(), '.git');
}
+ isInState(stateName) {
+ return this.state.constructor.name === stateName;
+ }
+
// Compound Getters //////////////////////////////////////////////////////////////////////////////////////////////////
// Accessor methods for data derived from other, state-provided getters.
From 5afdb22c25b911cfc4caeb477119e42b667fdaa1 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:37:24 -0400
Subject: [PATCH 075/161] Delegate a method to respond to filesystem changes
---
lib/models/repository.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 3cb3a22051..e3dfba0e99 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -172,6 +172,7 @@ const delegates = [
'clone',
'destroy',
'refresh',
+ 'observeFilesystemChange',
'stageFiles',
'unstageFiles',
From b011471f91a5804e39d424c35b4d9cba61cdd2fb Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:40:44 -0400
Subject: [PATCH 076/161] Adapt to the use of Real Objects for Branches,
Remotes, Commits
---
lib/controllers/commit-view-controller.js | 4 +--
lib/controllers/file-patch-controller.js | 2 +-
lib/controllers/github-tab-controller.js | 28 +++++++---------
lib/controllers/pr-info-controller.js | 6 ++--
lib/controllers/root-controller.js | 4 +--
lib/controllers/status-bar-tile-controller.js | 20 ++++++------
lib/models/repository-states/present.js | 18 +++++++----
lib/prop-types.js | 24 ++++++++------
lib/views/branch-menu-view.js | 25 ++++++---------
lib/views/branch-view.js | 6 ++--
lib/views/changed-files-count-view.js | 4 +--
lib/views/commit-view.js | 4 +--
lib/views/git-tab-view.js | 2 +-
lib/views/push-pull-menu-view.js | 18 +++++------
lib/views/staging-view.js | 2 +-
.../commit-view-controller.test.js | 4 ++-
test/controllers/git-tab-controller.test.js | 2 +-
.../status-bar-tile-controller.test.js | 32 ++++++++++++++-----
test/models/repository.test.js | 12 +++----
test/views/commit-view.test.js | 9 ++++--
20 files changed, 125 insertions(+), 101 deletions(-)
diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js
index 5d63054aa2..e75762f698 100644
--- a/lib/controllers/commit-view-controller.js
+++ b/lib/controllers/commit-view-controller.js
@@ -71,8 +71,8 @@ export default class CommitViewController {
let message = this.regularCommitMessage;
if (this.props.isAmending) {
message = this.amendingCommitMessage;
- if (!message.length && this.props.lastCommit) {
- message = this.props.lastCommit.message;
+ if (!message.length && this.props.lastCommit.isPresent()) {
+ message = this.props.lastCommit.getMessage();
}
} else if (!message.length && this.props.mergeMessage) {
message = this.props.mergeMessage;
diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js
index 1f19f71bfd..2722869510 100644
--- a/lib/controllers/file-patch-controller.js
+++ b/lib/controllers/file-patch-controller.js
@@ -11,7 +11,7 @@ import FilePatchView from '../views/file-patch-view';
export default class FilePatchController extends React.Component {
static propTypes = {
activeWorkingDirectory: PropTypes.string,
- repository: PropTypes.object,
+ repository: PropTypes.object.isRequired,
commandRegistry: PropTypes.object.isRequired,
filePatch: PropTypes.object.isRequired,
stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired,
diff --git a/lib/controllers/github-tab-controller.js b/lib/controllers/github-tab-controller.js
index bbf3b64ce3..537b340222 100644
--- a/lib/controllers/github-tab-controller.js
+++ b/lib/controllers/github-tab-controller.js
@@ -4,7 +4,6 @@ import {autobind} from 'core-decorators';
import yubikiri from 'yubikiri';
import RemotePrController from './remote-pr-controller';
-import Repository from '../models/repository';
import GithubLoginModel from '../models/github-login-model';
import ObserveModelDecorator from '../decorators/observe-model';
import {RemotePropType, BranchPropType} from '../prop-types';
@@ -27,9 +26,9 @@ class RemoteSelector extends React.Component {
{remotes.map(remote => (
- -
+
-
selectRemote(e, remote)}>
- {remote.name} ({remote.info.owner}/{remote.info.name})
+ {remote.getName()} ({remote.getOwner()}/{remote.getRepo()})
))}
@@ -43,12 +42,9 @@ class RemoteSelector extends React.Component {
getModel: props => props.repository,
fetchData: repo => {
return yubikiri({
- remotes: repo.getRemotes().then(remotes => {
- return remotes.map(({name, url}) => ({name, url, info: Repository.githubInfoFromRemote(url)}))
- .filter(remote => remote.info.githubRepo);
- }),
+ remotes: repo.getRemotes().then(remotes => remotes.filter(remote => remote.isGithubRepo())),
currentBranch: repo.getCurrentBranch(),
- selectedRemote: repo.getConfig('atomGithub.currentRemote'),
+ selectedRemoteName: repo.getConfig('atomGithub.currentRemote'),
});
},
})
@@ -57,13 +53,13 @@ export default class GithubTabController extends React.Component {
repository: PropTypes.object,
remotes: PropTypes.arrayOf(RemotePropType.isRequired),
currentBranch: BranchPropType,
- selectedRemote: PropTypes.string,
+ selectedRemoteName: PropTypes.string,
}
static defaultProps = {
remotes: null,
currentBranch: {name: '', isDetached: false},
- selectedRemote: null,
+ selectedRemoteName: null,
}
constructor(props, context) {
@@ -72,15 +68,15 @@ export default class GithubTabController extends React.Component {
}
render() {
- if (!this.props.repository || !this.props.remotes) {
+ if (!this.props.repository.isPresent() || this.props.remotes.length === 0) {
return null;
}
- if (!this.props.currentBranch || this.props.currentBranch.isDetached) {
+ if (!this.props.currentBranch.isPresent() || this.props.currentBranch.isDetached()) {
return null;
}
- let remote = this.props.remotes.find(r => r.name === this.props.selectedRemote);
+ let remote = this.props.remotes.find(r => r.getName() === this.props.selectedRemoteName);
let manyRemotesAvailable = false;
if (!remote && this.props.remotes.length === 1) {
remote = this.props.remotes[0];
@@ -97,13 +93,13 @@ export default class GithubTabController extends React.Component {
host="https://api.github.com"
loginModel={this.loginModel}
remote={remote}
- currentBranchName={this.props.currentBranch.name}
+ currentBranchName={this.props.currentBranch.getName()}
/>
}
{!remote && manyRemotesAvailable &&
}
@@ -128,6 +124,6 @@ export default class GithubTabController extends React.Component {
@autobind
handleRemoteSelect(e, remote) {
e.preventDefault();
- this.props.repository.setConfig('atomGithub.currentRemote', remote.name);
+ this.props.repository.setConfig('atomGithub.currentRemote', remote.getName());
}
}
diff --git a/lib/controllers/pr-info-controller.js b/lib/controllers/pr-info-controller.js
index 62c461271e..b61a6eccd2 100644
--- a/lib/controllers/pr-info-controller.js
+++ b/lib/controllers/pr-info-controller.js
@@ -24,7 +24,7 @@ export default class PrInfoController extends React.Component {
shouldComponentUpdate(nextProps) {
return (
nextProps.token !== this.props.token ||
- nextProps.hsot !== this.props.host ||
+ nextProps.host !== this.props.host ||
nextProps.currentBranchName !== this.props.currentBranchName ||
nextProps.onLogin !== this.props.onLogin ||
nextProps.remote !== this.props.remote
@@ -39,8 +39,8 @@ export default class PrInfoController extends React.Component {
}
const route = new PrInfoByBranchRoute({
- repoOwner: this.props.remote.info.owner,
- repoName: this.props.remote.info.name,
+ repoOwner: this.props.remote.getOwner(),
+ repoName: this.props.remote.getName(),
branchName: this.props.currentBranchName,
});
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index 68a9f311ac..cfe634e442 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -47,8 +47,8 @@ export default class RootController extends React.Component {
activeWorkingDirectory: PropTypes.string,
createRepositoryForProjectPath: PropTypes.func,
cloneRepositoryForProjectPath: PropTypes.func,
- repository: PropTypes.object,
- resolutionProgress: PropTypes.object,
+ repository: PropTypes.object.isRequired,
+ resolutionProgress: PropTypes.object.isRequired,
statusBar: PropTypes.object,
switchboard: PropTypes.instanceOf(Switchboard),
savedState: PropTypes.object,
diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js
index 1451d04ab7..f41f1a2539 100644
--- a/lib/controllers/status-bar-tile-controller.js
+++ b/lib/controllers/status-bar-tile-controller.js
@@ -35,9 +35,9 @@ import {autobind} from 'core-decorators';
return changedFiles.size;
}),
- remoteName: async query => repository.getRemoteForBranch((await query.currentBranch).name),
- aheadCount: async query => repository.getAheadCount((await query.currentBranch).name),
- behindCount: async query => repository.getBehindCount((await query.currentBranch).name),
+ remoteName: async query => repository.getRemoteForBranch((await query.currentBranch).getName()),
+ aheadCount: async query => repository.getAheadCount((await query.currentBranch).getName()),
+ behindCount: async query => repository.getBehindCount((await query.currentBranch).getName()),
});
},
})
@@ -47,9 +47,9 @@ export default class StatusBarTileController extends React.Component {
notificationManager: PropTypes.object.isRequired,
commandRegistry: PropTypes.object.isRequired,
tooltips: PropTypes.object.isRequired,
- repository: PropTypes.object,
- currentBranch: BranchPropType,
- branches: PropTypes.arrayOf(PropTypes.string),
+ repository: PropTypes.object.isRequired,
+ currentBranch: BranchPropType.isRequired,
+ branches: PropTypes.arrayOf(BranchPropType).isRequired,
remoteName: PropTypes.string,
aheadCount: PropTypes.number,
behindCount: PropTypes.number,
@@ -73,7 +73,7 @@ export default class StatusBarTileController extends React.Component {
render() {
const repoProps = {
- currentBranch: this.props.currentBranch || {name: '', isDetached: false},
+ currentBranch: this.props.currentBranch,
branches: this.props.branches,
remoteName: this.props.remoteName,
aheadCount: this.props.aheadCount,
@@ -179,17 +179,17 @@ export default class StatusBarTileController extends React.Component {
@autobind
async push(options) {
await this.setInProgressWhile(() => {
- return this.props.repository.push(this.props.currentBranch.name, options);
+ return this.props.repository.push(this.props.currentBranch.getName(), options);
}, {push: true});
}
@autobind
async pull() {
- await this.setInProgressWhile(() => this.props.repository.pull(this.props.currentBranch.name), {pull: true});
+ await this.setInProgressWhile(() => this.props.repository.pull(this.props.currentBranch.getName()), {pull: true});
}
@autobind
async fetch() {
- await this.setInProgressWhile(() => this.props.repository.fetch(this.props.currentBranch.name), {fetch: true});
+ await this.setInProgressWhile(() => this.props.repository.fetch(this.props.currentBranch.getName()), {fetch: true});
}
}
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index c22f1cf937..b93ab659cc 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -7,6 +7,9 @@ import FilePatch from '../file-patch';
import Hunk from '../hunk';
import HunkLine from '../hunk-line';
import DiscardHistory from '../discard-history';
+import Branch from '../branch';
+import Remote from '../remote';
+import Commit from '../commit';
class Cache {
constructor() {
@@ -324,8 +327,9 @@ export default class Present extends State {
// Commit access
getLastCommit() {
- return this.cache.getOrSet('last-commit', () => {
- return this.git().getHeadCommit();
+ return this.cache.getOrSet('last-commit', async () => {
+ const {sha, message, unbornRef} = await this.git().getHeadCommit();
+ return unbornRef ? Commit.createUnborn() : new Commit(sha, message);
});
}
@@ -338,8 +342,9 @@ export default class Present extends State {
}
getCurrentBranch() {
- return this.cache.getOrSet('current-branch', () => {
- return this.git().getCurrentBranch();
+ return this.cache.getOrSet('current-branch', async () => {
+ const {name, isDetached} = await this.git().getCurrentBranch();
+ return isDetached ? Branch.createDetached(name) : new Branch(name);
});
}
@@ -356,8 +361,9 @@ export default class Present extends State {
// Remotes
getRemotes() {
- return this.cache.getOrSet('remotes', () => {
- return this.git().getRemotes();
+ return this.cache.getOrSet('remotes', async () => {
+ const remotesInfo = await this.git().getRemotes();
+ return remotesInfo.map(({name, url}) => new Remote(name, url));
});
}
diff --git a/lib/prop-types.js b/lib/prop-types.js
index f95338d935..bce4df5ed5 100644
--- a/lib/prop-types.js
+++ b/lib/prop-types.js
@@ -11,16 +11,22 @@ export const DOMNodePropType = (props, propName, componentName) => {
};
export const RemotePropType = PropTypes.shape({
- name: PropTypes.string,
- url: PropTypes.string,
- info: PropTypes.shape({
- githubRepo: PropTypes.bool.isRequired,
- owner: PropTypes.string,
- name: PropTypes.string,
- }).isRequired,
+ getName: PropTypes.func.isRequired,
+ getUrl: PropTypes.func.isRequired,
+ isGithubRepo: PropTypes.func.isRequired,
+ getOwner: PropTypes.func.isRequired,
+ getRepo: PropTypes.func.isRequired,
});
export const BranchPropType = PropTypes.shape({
- name: PropTypes.string.isRequired,
- isDetached: PropTypes.bool.isRequired,
+ getName: PropTypes.func.isRequired,
+ isDetached: PropTypes.func.isRequired,
+ isPresent: PropTypes.func.isRequired,
+});
+
+export const CommitPropType = PropTypes.shape({
+ getSha: PropTypes.func.isRequired,
+ getMessage: PropTypes.func.isRequired,
+ isUnbornRef: PropTypes.func.isRequired,
+ isPresent: PropTypes.func.isRequired,
});
diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js
index 18c6ddecfd..3a8326ab68 100644
--- a/lib/views/branch-menu-view.js
+++ b/lib/views/branch-menu-view.js
@@ -11,17 +11,12 @@ export default class BranchMenuView extends React.Component {
workspace: PropTypes.object.isRequired,
commandRegistry: PropTypes.object.isRequired,
notificationManager: PropTypes.object.isRequired,
- branches: PropTypes.arrayOf(PropTypes.string),
- currentBranch: BranchPropType,
+ branches: PropTypes.arrayOf(BranchPropType).isRequired,
+ currentBranch: BranchPropType.isRequired,
checkout: PropTypes.func,
}
static defaultProps = {
- branches: [],
- currentBranch: {
- name: '',
- isDetached: true,
- },
checkout: () => Promise.resolve(),
}
@@ -35,10 +30,10 @@ export default class BranchMenuView extends React.Component {
}
render() {
- const currentBranchName = this.state.checkingOutBranch || this.props.currentBranch.name;
- const branches = this.props.branches;
- if (this.state.checkingOutBranch && this.props.branches.indexOf(this.state.checkingOutBranch) === -1) {
- branches.push(this.state.checkingOutBranch);
+ const currentBranchName = this.state.checkingOutBranch || this.props.currentBranch.getName();
+ const branchNames = this.props.branches.map(branch => branch.getName());
+ if (this.state.checkingOutBranch && branchNames.indexOf(this.state.checkingOutBranch) === -1) {
+ branchNames.push(this.state.checkingOutBranch);
}
const newBranchEditor = (
@@ -60,11 +55,11 @@ export default class BranchMenuView extends React.Component {
className="github-BranchMenuView-item github-BranchMenuView-select input-select"
onChange={this.didSelectItem}
value={currentBranchName}>
- {this.props.currentBranch.isDetached &&
-
+ {this.props.currentBranch.isDetached() &&
+
}
- {branches.map(branch => {
- return ;
+ {branchNames.map(branchName => {
+ return ;
})}
);
diff --git a/lib/views/branch-view.js b/lib/views/branch-view.js
index 06d70a96b4..e624e4dd49 100644
--- a/lib/views/branch-view.js
+++ b/lib/views/branch-view.js
@@ -5,18 +5,18 @@ import {BranchPropType} from '../prop-types';
export default class BranchView extends React.Component {
static propTypes = {
- currentBranch: BranchPropType,
+ currentBranch: BranchPropType.isRequired,
}
render() {
const classNames = cx(
- 'github-branch', 'inline-block', {'github-branch-detached': this.props.currentBranch.isDetached},
+ 'github-branch', 'inline-block', {'github-branch-detached': this.props.currentBranch.isDetached()},
);
return (
{ this.element = e; }}>
- {this.props.currentBranch.name}
+ {this.props.currentBranch.getName()}
);
}
diff --git a/lib/views/changed-files-count-view.js b/lib/views/changed-files-count-view.js
index 543855ea27..61eabe822f 100644
--- a/lib/views/changed-files-count-view.js
+++ b/lib/views/changed-files-count-view.js
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
export default class ChangedFilesCountView extends React.Component {
static propTypes = {
- changedFilesCount: PropTypes.number,
- didClick: PropTypes.func,
+ changedFilesCount: PropTypes.number.isRequired,
+ didClick: PropTypes.func.isRequired,
}
static defaultProps = {
diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js
index eb57d54cef..6a5c85dc1b 100644
--- a/lib/views/commit-view.js
+++ b/lib/views/commit-view.js
@@ -50,7 +50,7 @@ export default class CommitView {
}
const showAbortMergeButton = this.props.isMerging || null;
- const showAmendBox = (!this.props.isMerging && !this.props.lastCommit.unbornRef) || null;
+ const showAmendBox = (!this.props.isMerging && !this.props.lastCommit.isUnbornRef()) || null;
return (
@@ -133,7 +133,7 @@ export default class CommitView {
commitButtonText() {
if (this.props.isAmending) {
- return `Amend commit (${shortenSha(this.props.lastCommit.sha)})`;
+ return `Amend commit (${shortenSha(this.props.lastCommit.getSha())})`;
} else {
if (this.props.branchName) {
return `Commit to ${this.props.branchName}`;
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index b39d7b697b..7155fa0922 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -34,7 +34,7 @@ export default class GitTabView {
}
render() {
- if (!this.props.repository) {
+ if (this.props.repository.isEmpty()) {
return (
diff --git a/lib/views/push-pull-menu-view.js b/lib/views/push-pull-menu-view.js
index 5efd975df4..0064ec6d14 100644
--- a/lib/views/push-pull-menu-view.js
+++ b/lib/views/push-pull-menu-view.js
@@ -7,15 +7,15 @@ import {BranchPropType} from '../prop-types';
export default class PushPullMenuView extends React.Component {
static propTypes = {
+ notificationManager: PropTypes.object.isRequired,
+ currentBranch: BranchPropType.isRequired,
inProgress: PropTypes.bool,
- currentBranch: BranchPropType,
remoteName: PropTypes.string,
aheadCount: PropTypes.number,
behindCount: PropTypes.number,
- notificationManager: PropTypes.object,
- fetch: PropTypes.func,
- push: PropTypes.func,
- pull: PropTypes.func,
+ fetch: PropTypes.func.isRequired,
+ push: PropTypes.func.isRequired,
+ pull: PropTypes.func.isRequired,
}
static defaultProps = {
@@ -61,8 +61,8 @@ export default class PushPullMenuView extends React.Component {
render() {
const errorMessage = this.getErrorMessage();
const fetchDisabled = !this.props.remoteName || this.props.inProgress;
- const pullDisabled = !this.props.remoteName || this.props.currentBranch.isDetached || this.props.inProgress;
- const pushDisabled = this.props.currentBranch.isDetached || this.props.inProgress;
+ const pullDisabled = !this.props.remoteName || this.props.currentBranch.isDetached() || this.props.inProgress;
+ const pushDisabled = this.props.currentBranch.isDetached() || this.props.inProgress;
return (
@@ -99,12 +99,12 @@ export default class PushPullMenuView extends React.Component {
return this.state.errorMessage;
}
- if (this.props.currentBranch.isDetached) {
+ 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.currentBranch.name}. ` +
+ return `Note: No remote detected for branch ${this.props.currentBranch.getName()}. ` +
'Pushing will set up a remote tracking branch on remote repo "origin"';
}
diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js
index 06e53b354b..2379a21524 100644
--- a/lib/views/staging-view.js
+++ b/lib/views/staging-view.js
@@ -411,7 +411,7 @@ export default class StagingView {
Staged Changes
{
this.props.isAmending
- ? ` (amending ${shortenSha(this.props.lastCommit.sha)})`
+ ? ` (amending ${shortenSha(this.props.lastCommit.getSha())})`
: ''
}
diff --git a/test/controllers/commit-view-controller.test.js b/test/controllers/commit-view-controller.test.js
index e29206c8bd..32b00d12df 100644
--- a/test/controllers/commit-view-controller.test.js
+++ b/test/controllers/commit-view-controller.test.js
@@ -1,3 +1,5 @@
+import Commit from '../../lib/models/commit';
+
import CommitViewController from '../../lib/controllers/commit-view-controller';
import {cloneRepository, buildRepository} from '../helpers';
@@ -8,7 +10,7 @@ describe('CommitViewController', function() {
atomEnvironment = global.buildAtomEnvironment();
commandRegistry = atomEnvironment.commands;
- lastCommit = {sha: 'a1e23fd45', message: 'last commit message', unbornRef: false};
+ lastCommit = new Commit('a1e23fd45', 'last commit message');
});
afterEach(function() {
diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js
index 6a408e8471..663f4fd9b3 100644
--- a/test/controllers/git-tab-controller.test.js
+++ b/test/controllers/git-tab-controller.test.js
@@ -456,7 +456,7 @@ describe('GitTabController', function() {
await commitView.commit();
await controller.getLastModelDataRefreshPromise();
- assert.equal((await repository.getLastCommit()).message, 'Make it so');
+ assert.equal((await repository.getLastCommit()).getMessage(), 'Make it so');
});
it('can stage merge conflict files', async function() {
diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js
index ef7e8f43e9..8feedd44ff 100644
--- a/test/controllers/status-bar-tile-controller.test.js
+++ b/test/controllers/status-bar-tile-controller.test.js
@@ -97,17 +97,23 @@ describe('StatusBarTileController', function() {
const branches = Array.from(tip.getElementsByTagName('option'), e => e.innerHTML);
assert.deepEqual(branches, ['branch', 'master']);
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'master', isDetached: false});
+ const branch0 = await repository.getCurrentBranch();
+ assert.equal(branch0.getName(), 'master');
+ assert.isFalse(branch0.isDetached());
assert.equal(tip.querySelector('select').value, 'master');
selectOption(tip, 'branch');
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
+ const branch1 = await repository.getCurrentBranch();
+ assert.equal(branch1.getName(), 'branch');
+ assert.isFalse(branch1.isDetached());
assert.equal(tip.querySelector('select').value, 'branch');
await wrapper.instance().refreshModelData();
assert.equal(tip.querySelector('select').value, 'branch');
selectOption(tip, 'master');
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'master', isDetached: false});
+ const branch2 = await repository.getCurrentBranch();
+ assert.equal(branch2.getName(), 'master');
+ assert.isFalse(branch2.isDetached());
assert.equal(tip.querySelector('select').value, 'master');
await wrapper.instance().refreshModelData();
assert.equal(tip.querySelector('select').value, 'master');
@@ -130,7 +136,9 @@ describe('StatusBarTileController', function() {
const tip = getTooltipNode(wrapper, BranchView);
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
+ const branch0 = await repository.getCurrentBranch();
+ assert.equal(branch0.getName(), 'branch');
+ assert.isFalse(branch0.isDetached());
assert.equal(tip.querySelector('select').value, 'branch');
sinon.stub(notificationManager, 'addError');
@@ -162,7 +170,9 @@ describe('StatusBarTileController', function() {
const branches = Array.from(tip.querySelectorAll('option'), option => option.value);
assert.deepEqual(branches, ['master']);
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'master', isDetached: false});
+ const branch0 = await repository.getCurrentBranch();
+ assert.equal(branch0.getName(), 'master');
+ assert.isFalse(branch0.isDetached());
assert.equal(tip.querySelector('select').value, 'master');
tip.querySelector('button').click();
@@ -179,7 +189,9 @@ describe('StatusBarTileController', function() {
});
assert.equal(tip.querySelector('select').value, 'new-branch');
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'new-branch', isDetached: false});
+ const branch1 = await repository.getCurrentBranch();
+ assert.equal(branch1.getName(), 'new-branch');
+ assert.isFalse(branch1.isDetached());
assert.lengthOf(tip.querySelectorAll('.github-BranchMenuView-editor'), 0);
assert.lengthOf(tip.querySelectorAll('select'), 1);
@@ -198,7 +210,9 @@ describe('StatusBarTileController', function() {
const branches = Array.from(tip.getElementsByTagName('option'), option => option.value);
assert.deepEqual(branches, ['branch', 'master']);
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
+ const branch0 = await repository.getCurrentBranch();
+ assert.equal(branch0.getName(), 'branch');
+ assert.isFalse(branch0.isDetached());
assert.equal(tip.querySelector('select').value, 'branch');
tip.querySelector('button').click();
@@ -210,7 +224,9 @@ describe('StatusBarTileController', function() {
assert.equal(notificationArgs[0], 'Cannot create branch');
assert.match(notificationArgs[1].description, /already exists/);
- assert.deepEqual(await repository.getCurrentBranch(), {name: 'branch', isDetached: false});
+ const branch1 = await repository.getCurrentBranch();
+ assert.equal(branch1.getName(), 'branch');
+ assert.isFalse(branch1.isDetached());
assert.equal(tip.querySelector('select').value, 'branch');
});
});
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index bf4a422052..f765303896 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -275,7 +275,7 @@ describe('Repository', function() {
const repo = new Repository(workingDirPath);
await repo.getLoadPromise();
- assert.equal((await repo.getLastCommit()).message, 'Initial commit');
+ assert.equal((await repo.getLastCommit()).getMessage(), 'Initial commit');
fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8');
const unstagedPatch1 = await repo.getFilePatchForPath(path.join('subdir-1', 'a.txt'));
@@ -283,7 +283,7 @@ describe('Repository', function() {
repo.refresh();
await repo.applyPatchToIndex(unstagedPatch1);
await repo.commit('Commit 1');
- assert.equal((await repo.getLastCommit()).message, 'Commit 1');
+ assert.equal((await repo.getLastCommit()).getMessage(), 'Commit 1');
repo.refresh();
assert.deepEqual(await repo.getStagedChanges(), []);
const unstagedChanges = await repo.getUnstagedChanges();
@@ -292,7 +292,7 @@ describe('Repository', function() {
const unstagedPatch2 = await repo.getFilePatchForPath(path.join('subdir-1', 'a.txt'));
await repo.applyPatchToIndex(unstagedPatch2);
await repo.commit('Commit 2');
- assert.equal((await repo.getLastCommit()).message, 'Commit 2');
+ assert.equal((await repo.getLastCommit()).getMessage(), 'Commit 2');
repo.refresh();
assert.deepEqual(await repo.getStagedChanges(), []);
assert.deepEqual(await repo.getUnstagedChanges(), []);
@@ -334,7 +334,7 @@ describe('Repository', function() {
}
assert.equal(await repository.isMerging(), true);
- assert.equal((await repository.getLastCommit()).toString(), mergeBase.toString());
+ assert.equal((await repository.getLastCommit()).getSha(), mergeBase.getSha());
});
it('wraps the commit message body at 72 characters', async function() {
@@ -350,7 +350,7 @@ describe('Repository', function() {
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
].join('\n'));
- const message = (await repo.getLastCommit()).message;
+ const message = (await repo.getLastCommit()).getMessage();
assert.deepEqual(message.split('\n'), [
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
'',
@@ -374,7 +374,7 @@ describe('Repository', function() {
'# other stuff',
].join('\n'));
- assert.deepEqual((await repo.getLastCommit()).message, 'Make a commit');
+ assert.deepEqual((await repo.getLastCommit()).getMessage(), 'Make a commit');
});
});
diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js
index 5c42e20417..3ad0253eac 100644
--- a/test/views/commit-view.test.js
+++ b/test/views/commit-view.test.js
@@ -2,6 +2,7 @@ import {cloneRepository, buildRepository} from '../helpers';
import etch from 'etch';
import until from 'test-until';
+import Commit from '../../lib/models/commit';
import CommitView from '../../lib/views/commit-view';
describe('CommitView', function() {
@@ -11,7 +12,7 @@ describe('CommitView', function() {
atomEnv = global.buildAtomEnvironment();
commandRegistry = atomEnv.commands;
- lastCommit = {sha: '1234abcd', message: 'commit message', unbornRef: false};
+ lastCommit = new Commit('1234abcd', 'commit message');
});
afterEach(function() {
@@ -192,10 +193,12 @@ describe('CommitView', function() {
describe('amending', function() {
it('calls props.setAmending() when the box is checked or unchecked', function() {
+ const previousCommit = new Commit('111', "previous commit's message");
+
const setAmending = sinon.spy();
const view = new CommitView({
commandRegistry,
- stagedChangesExist: false, lastCommit: {message: 'previous commit\'s message'}, setAmending,
+ stagedChangesExist: false, lastCommit: previousCommit, setAmending,
});
const {amend} = view.refs;
@@ -210,7 +213,7 @@ describe('CommitView', function() {
const view = new CommitView({
commandRegistry,
stagedChangesExist: false,
- lastCommit: {unbornRef: true},
+ lastCommit: Commit.createUnborn(),
});
assert.isUndefined(view.refs.amend);
});
From 64843d5c4d0e69d96d3f6a3a74019093b6541167 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:44:53 -0400
Subject: [PATCH 077/161] Begin all ResolutionProgress instances empty
---
.../repository-conflict-controller.js | 2 +-
lib/models/conflicts/resolution-progress.js | 24 ++++++++++++-------
lib/views/staging-view.js | 2 +-
.../editor-conflict-controller.test.js | 2 +-
test/controllers/git-tab-controller.test.js | 4 ++--
.../conflicts/resolution-progress.test.js | 18 +++++++++-----
6 files changed, 33 insertions(+), 19 deletions(-)
diff --git a/lib/controllers/repository-conflict-controller.js b/lib/controllers/repository-conflict-controller.js
index 965a0bb1aa..758b366fe0 100644
--- a/lib/controllers/repository-conflict-controller.js
+++ b/lib/controllers/repository-conflict-controller.js
@@ -34,7 +34,7 @@ export default class RepositoryConflictController extends React.Component {
};
static defaultProps = {
- resolutionProgress: ResolutionProgress.empty(),
+ resolutionProgress: new ResolutionProgress(),
mergeConflictPaths: [],
isRebasing: false,
refreshResolutionProgress: () => {},
diff --git a/lib/models/conflicts/resolution-progress.js b/lib/models/conflicts/resolution-progress.js
index d135cc7dc0..bfd7664d83 100644
--- a/lib/models/conflicts/resolution-progress.js
+++ b/lib/models/conflicts/resolution-progress.js
@@ -1,21 +1,29 @@
import {Emitter} from 'event-kit';
export default class ResolutionProgress {
- static empty() {
- return new ResolutionProgress('', {});
- }
-
- constructor(revision, savedState) {
+ constructor() {
this.emitter = new Emitter();
- this.revision = revision;
+ this.revision = '';
this.markerCountByPath = new Map();
+ this.loaded = false;
+ }
+
+ load(revision, savedState) {
+ this.revision = revision;
if (savedState.revision === this.revision) {
- Object.keys(savedState.paths || {}).forEach(path => {
- this.markerCountByPath.set(path, savedState.paths[path]);
+ const paths = Object.keys(savedState.paths || {});
+ paths.forEach(filePath => {
+ this.markerCountByPath.set(filePath, savedState.paths[filePath]);
});
+
+ if (paths.length > 0) {
+ this.didUpdate();
+ }
}
+
+ this.loaded = true;
}
didUpdate() {
diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js
index 2379a21524..7a6a419983 100644
--- a/lib/views/staging-view.js
+++ b/lib/views/staging-view.js
@@ -443,7 +443,7 @@ export default class StagingView {
if (mergeConflicts && mergeConflicts.length > 0) {
const selectedItems = this.selection.getSelectedItems();
- const resolutionProgress = this.resolutionProgressObserver.getActiveModel() || ResolutionProgress.empty();
+ const resolutionProgress = this.resolutionProgressObserver.getActiveModel() || new ResolutionProgress();
const anyUnresolved = mergeConflicts
.map(conflict => path.join(this.props.workingDirectoryPath, conflict.filePath))
.some(conflictPath => resolutionProgress.getRemaining(conflictPath) !== 0);
diff --git a/test/controllers/editor-conflict-controller.test.js b/test/controllers/editor-conflict-controller.test.js
index e245834558..70c82f9b93 100644
--- a/test/controllers/editor-conflict-controller.test.js
+++ b/test/controllers/editor-conflict-controller.test.js
@@ -41,7 +41,7 @@ describe('EditorConflictController', function() {
commandRegistry = atomEnv.commands;
refreshResolutionProgress = sinon.spy();
- resolutionProgress = ResolutionProgress.empty();
+ resolutionProgress = new ResolutionProgress();
});
afterEach(function() {
diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js
index 663f4fd9b3..cfaa611b75 100644
--- a/test/controllers/git-tab-controller.test.js
+++ b/test/controllers/git-tab-controller.test.js
@@ -23,7 +23,7 @@ describe('GitTabController', function() {
workspaceElement = atomEnvironment.views.getView(workspace);
- resolutionProgress = ResolutionProgress.empty();
+ resolutionProgress = new ResolutionProgress();
refreshResolutionProgress = sinon.spy();
});
@@ -120,7 +120,7 @@ describe('GitTabController', function() {
const repository = await buildRepository(workdirPath);
await assert.isRejected(repository.git.merge('origin/branch'));
- const rp = ResolutionProgress.empty();
+ const rp = new ResolutionProgress();
rp.reportMarkerCount(path.join(workdirPath, 'added-to-both.txt'), 5);
const controller = new GitTabController({
diff --git a/test/models/conflicts/resolution-progress.test.js b/test/models/conflicts/resolution-progress.test.js
index 874433ed0c..6f71983071 100644
--- a/test/models/conflicts/resolution-progress.test.js
+++ b/test/models/conflicts/resolution-progress.test.js
@@ -2,12 +2,14 @@ import ResolutionProgress from '../../../lib/models/conflicts/resolution-progres
describe('ResolutionProgress', function() {
it('reports undefined for any path that has not reported progress yet', function() {
- const progress = new ResolutionProgress('1234abcd', {});
+ const progress = new ResolutionProgress();
+ progress.load('1234abcd', {});
assert.isUndefined(progress.getRemaining('path/to/file.txt'));
});
it('accepts reports of unresolved conflict counts', function() {
- const progress = new ResolutionProgress('1234abcd', {});
+ const progress = new ResolutionProgress();
+ progress.load('1234abcd', {});
progress.reportMarkerCount('path/to/file.txt', 3);
assert.equal(progress.getRemaining('path/to/file.txt'), 3);
@@ -17,7 +19,8 @@ describe('ResolutionProgress', function() {
let payload;
beforeEach(function() {
- const progress0 = new ResolutionProgress('1234abcd', {});
+ const progress0 = new ResolutionProgress();
+ progress0.load('1234abcd', {});
progress0.reportMarkerCount('path/to/file0.txt', 3);
progress0.reportMarkerCount('path/to/file1.txt', 4);
@@ -25,14 +28,16 @@ describe('ResolutionProgress', function() {
});
it('restores data from the same revision', function() {
- const progress1 = new ResolutionProgress('1234abcd', payload);
+ const progress1 = new ResolutionProgress();
+ progress1.load('1234abcd', payload);
assert.equal(progress1.getRemaining('path/to/file0.txt'), 3);
assert.equal(progress1.getRemaining('path/to/file1.txt'), 4);
assert.isFalse(progress1.isEmpty());
});
it('restores an empty object for a different revision', function() {
- const progress2 = new ResolutionProgress('abcd1234', payload);
+ const progress2 = new ResolutionProgress();
+ progress2.load('abcd1234', payload);
assert.isUndefined(progress2.getRemaining('path/to/file0.txt'));
assert.isUndefined(progress2.getRemaining('path/to/file1.txt'));
@@ -44,7 +49,8 @@ describe('ResolutionProgress', function() {
let progress, didUpdateSpy;
beforeEach(function() {
- progress = new ResolutionProgress('1234abcd', {});
+ progress = new ResolutionProgress();
+ progress.load('1234abcd', {});
progress.reportMarkerCount('path/file0.txt', 4);
didUpdateSpy = sinon.spy();
From a2e01f0fb1b3000b7ac394f231669b678c3d0e4f Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:45:09 -0400
Subject: [PATCH 078/161] Utility methods on change observers
---
lib/models/file-system-change-observer.js | 13 +++++++++++++
lib/models/workspace-change-observer.js | 13 +++++++++++++
2 files changed, 26 insertions(+)
diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js
index 870a904048..1a9624119d 100644
--- a/lib/models/file-system-change-observer.js
+++ b/lib/models/file-system-change-observer.js
@@ -10,6 +10,7 @@ export default class FileSystemChangeObserver {
this.emitter = new Emitter();
this.repository = repository;
this.logger = new EventLogger('FileSystemChangeObserver');
+ this.started = false;
}
async start() {
@@ -24,6 +25,18 @@ export default class FileSystemChangeObserver {
await this.stopCurrentFileWatcher();
}
+ isStarted() {
+ return this.started;
+ }
+
+ didChange(payload) {
+ this.emitter.emit('did-change', payload);
+ }
+
+ didChangeWorkdirOrHead() {
+ this.emitter.emit('did-change-workdir-or-head');
+ }
+
onDidChange(callback) {
return this.emitter.on('did-change', callback);
}
diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js
index 2b3a0ac4fa..b920d0e74e 100644
--- a/lib/models/workspace-change-observer.js
+++ b/lib/models/workspace-change-observer.js
@@ -14,6 +14,7 @@ export default class WorkspaceChangeObserver {
this.emitter = new Emitter();
this.disposables = new CompositeDisposable();
this.logger = new EventLogger('WorkspaceChangeObserver');
+ this.started = false;
}
async start() {
@@ -41,6 +42,18 @@ export default class WorkspaceChangeObserver {
await this.stopCurrentFileWatcher();
}
+ isStarted() {
+ return this.started;
+ }
+
+ didChange(payload) {
+ this.emitter.emit('did-change', payload);
+ }
+
+ didChangeWorkdirOrHead() {
+ this.emitter.emit('did-change-workdir-or-head');
+ }
+
onDidChange(callback) {
return this.emitter.on('did-change', callback);
}
From 9f48f95cc389334c983bf55c29b8619a6c014e8a Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:45:18 -0400
Subject: [PATCH 079/161] Register the Destroyed state
---
lib/models/repository-states/destroyed.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/models/repository-states/destroyed.js b/lib/models/repository-states/destroyed.js
index cf48c7b252..5144f35bba 100644
--- a/lib/models/repository-states/destroyed.js
+++ b/lib/models/repository-states/destroyed.js
@@ -14,3 +14,5 @@ export default class Destroyed extends State {
// No-op to destroy twice
}
}
+
+State.register(Destroyed);
From 9f386d2ba01589b5b42807b6d9b2e78d0d9b4ad8 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:45:34 -0400
Subject: [PATCH 080/161] Default observeFilesystemChange() to a refresh()
---
lib/models/repository-states/state.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index e0dd1afc30..6ea7313cbb 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -81,6 +81,11 @@ export default class State {
// No-op
}
+ @shouldDelegate
+ observeFilesystemChange() {
+ this.refresh();
+ }
+
// Git operations ////////////////////////////////////////////////////////////////////////////////////////////////////
// These default to rejecting a Promise with an error stating that the operation is not supported in the current
// state.
From caf499bfc51db7ec91a142fd2f2d1240b41f60f2 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:47:27 -0400
Subject: [PATCH 081/161] WorkdirContext no longer needs to defer state
---
lib/models/workdir-context.js | 214 +++++++++++-----------------
test/models/workdir-context.test.js | 156 +++++++++-----------
2 files changed, 157 insertions(+), 213 deletions(-)
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 408a24232a..737daa4ded 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -1,48 +1,12 @@
import {Emitter, CompositeDisposable} from 'event-kit';
+import {autobind} from 'core-decorators';
import Repository from './repository';
import ResolutionProgress from './conflicts/resolution-progress';
import FileSystemChangeObserver from './file-system-change-observer';
import WorkspaceChangeObserver from './workspace-change-observer';
-// Provide both synchronous and asynchronous access to a value.
-class Deferred {
- constructor(value = null) {
- if (value !== null) {
- this.value = value;
- this.promise = Promise.resolve(value);
- this.resolve = () => {
- throw new Error('Attempt to resolve Deferred with concrete value');
- };
- this.reject = () => {
- throw new Error('Attempt to reject Deferred with concrete value');
- };
- } else {
- this.value = null;
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- }
- }
-
- hasValue() {
- return this.value !== null;
- }
-
- setValue(value) {
- this.value = value;
- this.resolve(value);
- }
-
- getValue() {
- return this.value;
- }
-
- getPromise() {
- return this.promise;
- }
-}
+const DORMANT = {};
/*
* Bundle of model objects associated with a git working directory.
@@ -64,69 +28,60 @@ export default class WorkdirContext {
*/
constructor(directory, options = {}) {
this.directory = directory;
- this.options = options;
-
- this.repositoryHolder = new Deferred(this.options.repository || null);
- this.changeObserverHolder = new Deferred();
- this.resolutionProgressHolder = new Deferred();
+ const {repository, window: theWindow, workspace, promptCallback} = options;
+ this.savedResolutionProgress = options.savedResolutionProgress || {};
this.destroyed = false;
this.emitter = new Emitter();
this.subs = new CompositeDisposable();
- }
- /*
- * Initialize all model objects associated with this git working directory. Return a Promise that will resolve when
- * all models are available and ready, including the filesystem watcher. Outside of test cases, it's generally
- * better to not await this Promise and instead await the `getXyzPromise()` of the models that you need.
- */
- async create() {
- if (this.destroyed) {
- return;
- }
+ this.repository = repository || new Repository(directory);
+ this.observer = process.platform === 'linux'
+ ? new WorkspaceChangeObserver(theWindow, workspace, this.repository)
+ : new FileSystemChangeObserver(this.repository);
+ this.resolutionProgress = new ResolutionProgress();
- let repo;
- if (!this.repositoryHolder.hasValue()) {
- repo = await Repository.open(this.directory);
- this.repositoryHolder.setValue(repo);
- } else {
- repo = this.getRepository();
+ if (promptCallback) {
+ this.repository.setPromptCallback(promptCallback);
}
- if (!repo) {
- this.changeObserverHolder.setValue(null);
- this.resolutionProgressHolder.setValue(null);
- return;
- }
+ if (options.dormant !== DORMANT) {
+ // Wire up event forwarding among models
+ this.subs.add(this.repository.onDidChangeState(this.repositoryChangedState));
+ this.subs.add(this.observer.onDidChange(payload => this.repository.observeFilesystemChange(payload)));
+ this.subs.add(this.observer.onDidChangeWorkdirOrHead(() => this.emitter.emit('did-change-workdir-or-head')));
- if (this.options.promptCallback) {
- repo.setPromptCallback(this.options.promptCallback);
+ // If a pre-loaded Repository was provided, broadcast an initial state change event.
+ if (this.repository) {
+ this.repositoryChangedState({from: null, to: this.repository.state});
+ }
}
+ }
- // Event forwarding from repository
- this.subs.add(repo.onDidDestroy(() => this.emitter.emit('did-destroy-repository')));
- this.subs.add(repo.onDidUpdate(() => this.emitter.emit('did-update-repository')));
-
- const changeObserver = process.platform === 'linux'
- ? new WorkspaceChangeObserver(this.options.window, this.options.workspace, repo)
- : new FileSystemChangeObserver(repo);
-
- // Event forwarding from change observer
- this.subs.add(changeObserver.onDidChange(() => repo.refresh()));
- this.subs.add(changeObserver.onDidChangeWorkdirOrHead(() => this.emitter.emit('did-change-workdir-or-head')));
+ /**
+ * Respond to changes in `Repository` state. Load resolution progress and start the change observer when it becomes
+ * present. Stop the change observer when it is destroyed. Re-broadcast the event to context subscribers
+ * regardless.
+ *
+ * The ResolutionProgress will be loaded before the change event is re-broadcast, but change observer modifications
+ * will not be complete.
+ */
+ @autobind
+ async repositoryChangedState(payload) {
+ if (this.destroyed) {
+ return;
+ }
- this.changeObserverHolder.setValue(changeObserver);
+ if (this.repository.isPresent()) {
+ this.observer.start().then(() => this.emitter.emit('did-start-observer'));
- const populateResolutionProgress = async () => {
- const commit = await repo.getLastCommit();
- const progress = new ResolutionProgress(commit.sha, this.options.savedResolutionProgress || {});
- this.resolutionProgressHolder.setValue(progress);
- };
+ const head = await this.repository.getLastCommit();
+ this.resolutionProgress.load(head.getSha(), this.savedResolutionProgress);
+ } else if (this.repository.isDestroyed()) {
+ this.observer.destroy();
+ }
- await Promise.all([
- populateResolutionProgress(),
- changeObserver.start(),
- ]);
+ this.emitter.emit('did-change-repository-state', payload);
}
isPresent() {
@@ -137,13 +92,20 @@ export default class WorkdirContext {
return this.destroyed;
}
- /*
- * Subscribe to the filesystem observer event.
- */
+ // Event subscriptions
+
+ onDidStartObserver(callback) {
+ return this.emitter.on('did-start-observer', callback);
+ }
+
onDidChangeWorkdirOrHead(callback) {
return this.emitter.on('did-change-workdir-or-head', callback);
}
+ onDidChangeRepositoryState(callback) {
+ return this.emitter.on('did-change-repository-state', callback);
+ }
+
onDidUpdateRepository(callback) {
return this.emitter.on('did-update-repository', callback);
}
@@ -152,32 +114,48 @@ export default class WorkdirContext {
return this.emitter.on('did-destroy-repository', callback);
}
- getWorkingDirectory() {
- return this.directory;
+ /**
+ * Return a Promise that will resolve the next time that a Repository transitions to the requested state. Most
+ * useful for test cases; most callers should prefer subscribing to `onDidChangeRepositoryState`.
+ */
+ getRepositoryStatePromise(stateName) {
+ return new Promise(resolve => {
+ const sub = this.onDidChangeRepositoryState(() => {
+ if (this.repository.isInState(stateName)) {
+ resolve();
+ sub.dispose();
+ }
+ });
+ });
}
- getRepository() {
- return this.repositoryHolder.getValue();
+ /**
+ * Return a Promise that will resolve the next time that a ChangeObserver successfully starts. Most useful for
+ * test cases.
+ */
+ getObserverStartedPromise() {
+ return new Promise(resolve => {
+ const sub = this.onDidStartObserver(() => {
+ resolve();
+ sub.dispose();
+ });
+ });
}
- getRepositoryPromise() {
- return this.repositoryHolder.getPromise();
+ getWorkingDirectory() {
+ return this.directory;
}
- getChangeObserver() {
- return this.changeObserverHolder.getValue();
+ getRepository() {
+ return this.repository;
}
- getChangeObserverPromise() {
- return this.changeObserverHolder.getPromise();
+ getChangeObserver() {
+ return this.observer;
}
getResolutionProgress() {
- return this.resolutionProgressHolder.getValue();
- }
-
- getResolutionProgressPromise() {
- return this.resolutionProgressHolder.getPromise();
+ return this.resolutionProgress;
}
/*
@@ -187,40 +165,22 @@ export default class WorkdirContext {
if (this.destroyed) {
return;
}
-
this.destroyed = true;
+
+ this.repository.destroy();
this.subs.dispose();
this.emitter.dispose();
- const observer = this.getChangeObserver();
- const repository = this.getRepository();
-
- repository && repository.destroy();
-
- if (observer) {
- await observer.destroy();
- }
+ await this.observer.destroy();
}
}
export class NullWorkdirContext extends WorkdirContext {
constructor(directory) {
- super(directory, {});
-
- this.repositoryHolder.setValue(null);
- this.changeObserverHolder.setValue(null);
- this.resolutionProgressHolder.setValue(null);
- }
-
- create() {
- return Promise.resolve();
+ super(directory, {dormant: DORMANT});
}
isPresent() {
return false;
}
-
- destroy() {
- return Promise.resolve();
- }
}
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 34546d689d..070593f6ce 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -1,28 +1,43 @@
import {CompositeDisposable, Disposable} from 'event-kit';
-import {cloneRepository} from '../helpers';
+import {cloneRepository, sha} from '../helpers';
import WorkdirContext from '../../lib/models/workdir-context';
import Repository from '../../lib/models/repository';
describe('WorkdirContext', function() {
- let context, workingDirectory, subs;
- let mockWindow, mockWorkspace, mockPromptCallback;
+ let context, workingDirectory, subs, options;
+ let mockPromptCallback;
beforeEach(async function() {
workingDirectory = await cloneRepository('three-files');
subs = new CompositeDisposable();
- mockWindow = {
+ const mockWindow = {
addEventListener: sinon.spy(),
removeEventListener: sinon.spy(),
};
- mockWorkspace = {
+ const mockWorkspace = {
observeTextEditors: sinon.stub().returns(new Disposable()),
};
+ const mockSavedResolutionProgress = {
+ revision: await sha(workingDirectory),
+ paths: {
+ 'a.txt': 2,
+ 'b.txt': 3,
+ },
+ };
+
mockPromptCallback = query => 'reply';
+
+ options = {
+ window: mockWindow,
+ workspace: mockWorkspace,
+ promptCallback: mockPromptCallback,
+ savedResolutionProgress: mockSavedResolutionProgress,
+ };
});
afterEach(async function() {
@@ -32,115 +47,86 @@ describe('WorkdirContext', function() {
describe('without an initial Repository', function() {
beforeEach(function() {
- context = new WorkdirContext(workingDirectory, {
- window: mockWindow,
- workspace: mockWorkspace,
- promptCallback: mockPromptCallback,
- });
+ context = new WorkdirContext(workingDirectory, options);
});
- it('returns null for each synchronous model', function() {
- assert.isNull(context.getRepository());
- assert.isNull(context.getResolutionProgress());
- assert.isNull(context.getChangeObserver());
- });
-
- it('populates the repository on create()', async function() {
- const repoPromise = context.getRepositoryPromise();
-
- await context.create();
-
- const repo = context.getRepository();
- assert.isNotNull(repo);
- assert.equal(repo.getWorkingDirectoryPath(), workingDirectory);
- assert.strictEqual(repo, await repoPromise);
+ it('returns synchronous models in their initial states', function() {
+ assert.isTrue(context.getRepository().isLoading());
+ assert.isTrue(context.getResolutionProgress().isEmpty());
+ assert.isFalse(context.getChangeObserver().isStarted());
});
- it('populates the resolution progress on create()', async function() {
- const progressPromise = context.getResolutionProgressPromise();
-
- await context.create();
-
+ it('updates the resolution progress once the repository loads', async function() {
const progress = context.getResolutionProgress();
- assert.isNotNull(progress);
- assert.strictEqual(progress, await progressPromise);
- });
- it('populates the change observer on create()', async function() {
- const observerPromise = context.getChangeObserverPromise();
+ await context.getRepositoryStatePromise('Present');
- await context.create();
+ assert.isFalse(progress.isEmpty());
+ assert.equal(progress.getRemaining('a.txt'), 2);
+ assert.equal(progress.getRemaining('b.txt'), 3);
+ });
+ it('starts the change observer after the repository loads', async function() {
const observer = context.getChangeObserver();
- assert.isNotNull(observer);
- assert.strictEqual(observer, await observerPromise);
+ await context.getObserverStartedPromise();
+ assert.isTrue(observer.isStarted());
});
it('configures the repository with a prompt callback', async function() {
- await context.create();
-
const repo = context.getRepository();
+ await repo.getLoadPromise();
+
for (const strategy of repo.git.getImplementers()) {
assert.strictEqual(strategy.prompt, mockPromptCallback);
}
});
- it('refreshes the repository on any filesystem change', async function() {
- await context.create();
-
+ it('notifies the repository on any filesystem change', async function() {
const repo = context.getRepository();
- sinon.spy(repo, 'refresh');
+ await context.getRepositoryStatePromise('Present');
- const observer = context.getChangeObserver();
- observer.emitter.emit('did-change');
+ sinon.spy(repo, 'observeFilesystemChange');
+
+ const payload = {};
+ context.getChangeObserver().didChange(payload);
- assert.isTrue(repo.refresh.called);
+ assert.isTrue(repo.observeFilesystemChange.calledWith(payload));
});
- it('emits an event on workdir or head change', async function() {
+ it('re-emits an event on workdir or head change', async function() {
const listener = sinon.spy();
subs.add(context.onDidChangeWorkdirOrHead(listener));
- await context.create();
+ await context.getRepositoryStatePromise('Present');
- context.getChangeObserver().emitter.emit('did-change-workdir-or-head');
+ context.getChangeObserver().didChangeWorkdirOrHead();
assert.isTrue(listener.called);
});
- it('can be destroyed before create()', async function() {
- await context.destroy();
- assert.isTrue(context.isDestroyed());
-
- await context.create();
- assert.isTrue(context.isDestroyed());
- });
-
it('destroys the repository on destroy()', async function() {
- await context.create();
+ await context.getRepositoryStatePromise('Present');
const repo = context.getRepository();
- sinon.spy(repo, 'destroy');
await context.destroy();
- assert.isTrue(repo.destroy.called);
+ assert.isTrue(repo.isDestroyed());
});
it('stops the change observer on destroy()', async function() {
- await context.create();
const observer = context.getChangeObserver();
await observer.start();
- sinon.spy(observer, 'destroy');
-
await context.destroy();
- assert.isTrue(observer.destroy.called);
+ assert.isFalse(observer.isStarted());
});
it('can be destroyed twice', async function() {
- await context.create();
assert.isFalse(context.isDestroyed());
await context.destroy();
assert.isTrue(context.isDestroyed());
+
+ await context.destroy();
+ assert.isTrue(context.isDestroyed());
});
});
@@ -148,37 +134,35 @@ describe('WorkdirContext', function() {
let repository;
beforeEach(async function() {
- repository = await Repository.open(workingDirectory);
- context = new WorkdirContext(workingDirectory, {
- repository,
- window: mockWindow,
- workspace: mockWorkspace,
- promptCallback: mockPromptCallback,
- });
+ repository = new Repository(workingDirectory);
+ await repository.getLoadPromise();
+ sinon.spy(repository, 'getLastCommit');
+ options.repository = repository;
+
+ context = new WorkdirContext(workingDirectory, options);
});
- it('returns the repository synchrounsly', function() {
+ it('returns the repository synchronously', function() {
assert.strictEqual(repository, context.getRepository());
});
- it('populates the resolution progress on create()', async function() {
- const progressPromise = context.getResolutionProgressPromise();
+ it('begins populating the resolution progress immediately', async function() {
+ assert.isTrue(repository.getLastCommit.called);
- await context.create();
+ await context.getRepositoryStatePromise('Present');
const progress = context.getResolutionProgress();
- assert.isNotNull(progress);
- assert.strictEqual(progress, await progressPromise);
+ assert.isFalse(progress.isEmpty());
+ assert.equal(progress.getRemaining('a.txt'), 2);
+ assert.equal(progress.getRemaining('b.txt'), 3);
});
- it('populates the change observer on create()', async function() {
- const observerPromise = context.getChangeObserverPromise();
-
- await context.create();
-
+ it('starts the change observer automatically', async function() {
const observer = context.getChangeObserver();
- assert.isNotNull(observer);
- assert.strictEqual(observer, await observerPromise);
+ assert.isFalse(observer.isStarted());
+
+ await context.getObserverStartedPromise();
+ assert.isTrue(observer.isStarted());
});
});
});
From ef8417691620d3718e5910c665ef2d45a77ec0d5 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 11:48:29 -0400
Subject: [PATCH 082/161] Manage WorkdirContext lifecycle correctly within a
pool
---
lib/models/workdir-context-pool.js | 32 +++++++-----------
test/models/workdir-context-pool.test.js | 42 +++++++++++++-----------
2 files changed, 33 insertions(+), 41 deletions(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index 23e1cf40b2..c513764250 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -48,24 +48,18 @@ export default class WorkdirContextPool {
const emit = () => this.emitter.emit(emitEventName, context);
disposable.add(context[subMethod](emit));
};
+
+ forwardEvent('onDidStartObserver', 'did-start-observer');
forwardEvent('onDidChangeWorkdirOrHead', 'did-change-workdir-or-head');
+ forwardEvent('onDidChangeRepositoryState', 'did-change-repository-state');
forwardEvent('onDidUpdateRepository', 'did-update-repository');
forwardEvent('onDidDestroyRepository', 'did-destroy-repository');
- const forwardPromise = (subMethod, emitEventName) => {
- context[subMethod]().then(() => this.emitter.emit(emitEventName, context));
- };
- forwardPromise('getRepositoryPromise', 'did-load-repository');
- forwardPromise('getResolutionProgressPromise', 'did-load-resolution-progress');
- forwardPromise('getChangeObserverPromise', 'did-load-change-observer');
-
disposable.add(this.onDidRemoveContext(removed => {
if (removed === context) {
disposable.dispose();
}
}));
-
- context.create();
}
replace(directory, options = {}) {
@@ -101,20 +95,20 @@ export default class WorkdirContextPool {
}
}
- onDidRemoveContext(callback) {
- return this.emitter.on('did-remove-context', callback);
+ onDidStartObserver(callback) {
+ return this.emitter.on('did-start-observer', callback);
}
- onDidLoadRepository(callback) {
- return this.emitter.on('did-load-repository', callback);
+ onDidChangeWorkdirOrHead(callback) {
+ return this.emitter.on('did-change-workdir-or-head', callback);
}
- onDidLoadResolutionProgress(callback) {
- return this.emitter.on('did-load-resolution-progress', callback);
+ onDidRemoveContext(callback) {
+ return this.emitter.on('did-remove-context', callback);
}
- onDidLoadChangeObserver(callback) {
- return this.emitter.on('did-load-change-observer', callback);
+ onDidChangeRepositoryState(callback) {
+ return this.emitter.on('did-change-repository-state', callback);
}
onDidUpdateRepository(callback) {
@@ -125,10 +119,6 @@ export default class WorkdirContextPool {
return this.emitter.on('did-destroy-repository', callback);
}
- onDidChangeWorkdirOrHead(callback) {
- return this.emitter.on('did-change-workdir-or-head', callback);
- }
-
clear() {
this.withResidentContexts(workdir => this.remove(workdir));
}
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index 38d926127e..25ca8a4552 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -1,6 +1,6 @@
import temp from 'temp';
-import {cloneRepository, buildRepository} from '../helpers';
+import {cloneRepository, buildRepository, sha} from '../helpers';
import Repository from '../../lib/models/repository';
import WorkdirContextPool from '../../lib/models/workdir-context-pool';
@@ -42,8 +42,8 @@ describe('WorkdirContextPool', function() {
assert.isTrue(pool.getContext(workingDirectory).isPresent());
});
- it('optionally provides a preinitialized repository', async function() {
- const existingRepo = await Repository.open(workingDirectory);
+ it('optionally provides a preinitialized repository', function() {
+ const existingRepo = new Repository(workingDirectory);
pool.add(workingDirectory, {repository: existingRepo});
@@ -65,24 +65,27 @@ describe('WorkdirContextPool', function() {
it('begins but does not await the asynchronous initialization process', async function() {
pool.add(workingDirectory);
const context = pool.getContext(workingDirectory);
- assert.isNull(context.getRepository());
+ assert.isTrue(context.getRepository().isLoading());
+ assert.isTrue(context.getResolutionProgress().isEmpty());
+ assert.isFalse(context.getChangeObserver().isStarted());
- const repo = await context.getRepositoryPromise();
- assert.isNotNull(repo);
+ await context.getRepositoryStatePromise('Present');
+ assert.isTrue(context.getRepository().isPresent());
});
it('passes appropriate serialized state to the resolution progress', async function() {
const resolutionProgressByPath = {
[workingDirectory]: {
- revision: '66d11860af6d28eb38349ef83de475597cb0e8b4',
+ revision: await sha(workingDirectory),
paths: {'a.txt': 12},
},
};
pool.add(workingDirectory, {resolutionProgressByPath});
+ const context = pool.getContext(workingDirectory);
+ await context.getRepositoryStatePromise('Present');
- const resolutionProgress = await pool.getContext(workingDirectory).getResolutionProgressPromise();
- assert.isNotNull(resolutionProgress);
+ const resolutionProgress = context.getResolutionProgress();
assert.equal(resolutionProgress.getRemaining('a.txt'), 12);
});
});
@@ -105,13 +108,14 @@ describe('WorkdirContextPool', function() {
pool.add(directory);
assert.equal(pool.size(), 1);
- assert.isTrue(pool.getContext(directory).isPresent());
- const original = await pool.getContext(directory).getRepositoryPromise();
+ const context = pool.getContext(directory);
+ assert.isTrue(context.isPresent());
+ const original = context.getRepository();
pool.replace(directory);
assert.equal(pool.size(), 1);
assert.isTrue(pool.getContext(directory).isPresent());
- const replaced = await pool.getContext(directory).getRepositoryPromise();
+ const replaced = pool.getContext(directory).getRepository();
assert.notStrictEqual(original, replaced);
});
@@ -122,14 +126,14 @@ describe('WorkdirContextPool', function() {
pool.add(directory);
assert.equal(pool.size(), 1);
assert.isTrue(pool.getContext(directory).isPresent());
- const original = await pool.getContext(directory).getRepositoryPromise();
+ const original = pool.getContext(directory).getRepository();
const replacement = await buildRepository(directory);
pool.replace(directory, {repository: replacement});
assert.equal(pool.size(), 1);
assert.isTrue(pool.getContext(directory).isPresent());
- const replaced = await pool.getContext(directory).getRepositoryPromise();
+ const replaced = pool.getContext(directory).getRepository();
assert.notStrictEqual(original, replaced);
assert.strictEqual(replacement, replaced);
@@ -159,8 +163,8 @@ describe('WorkdirContextPool', function() {
assert.equal(pool.size(), 1);
});
- it('begins but does not await the termination process', async function() {
- const repo = await existingContext.getRepositoryPromise();
+ it('begins but does not await the termination process', function() {
+ const repo = existingContext.getRepository();
sinon.spy(repo, 'destroy');
assert.isFalse(existingContext.isDestroyed());
@@ -199,8 +203,6 @@ describe('WorkdirContextPool', function() {
assert.isTrue(context0.isDestroyed());
assert.isFalse(context1.isDestroyed());
});
-
- it('passes appropriate serialized state to any created resolution progresses');
});
describe('getContext', function() {
@@ -211,11 +213,11 @@ describe('WorkdirContextPool', function() {
pool.add(dir);
});
- it('returns a context by directory', async function() {
+ it('returns a context by directory', function() {
const context = pool.getContext(dir);
assert.isTrue(context.isPresent());
- const repo = await context.getRepositoryPromise();
+ const repo = context.getRepository();
assert.strictEqual(dir, repo.getWorkingDirectoryPath());
});
From d428db30b85022dead5865b832f7183cd6bcb57b Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Fri, 14 Apr 2017 13:09:22 -0400
Subject: [PATCH 083/161] Introduce an "Absent" state when no working directory
is available
---
lib/models/repository-states/absent.js | 22 +++++++++++++++++
lib/models/repository-states/index.js | 1 +
lib/models/repository-states/state.js | 5 ++++
lib/models/repository.js | 33 +++++++++++++++++---------
test/models/repository.test.js | 19 +++++++++++++++
5 files changed, 69 insertions(+), 11 deletions(-)
create mode 100644 lib/models/repository-states/absent.js
diff --git a/lib/models/repository-states/absent.js b/lib/models/repository-states/absent.js
new file mode 100644
index 0000000000..7f21806f4c
--- /dev/null
+++ b/lib/models/repository-states/absent.js
@@ -0,0 +1,22 @@
+import State from './state';
+import Empty from './empty';
+
+export default class Absent extends Empty {
+ isAbsent() {
+ return true;
+ }
+
+ init() {
+ return disallowed('init');
+ }
+
+ clone() {
+ return disallowed('clone');
+ }
+}
+
+State.register(Absent);
+
+function disallowed(actionName) {
+ return Promise.reject(new Error(`${actionName} cannot be performed on an absent repository`));
+}
diff --git a/lib/models/repository-states/index.js b/lib/models/repository-states/index.js
index e57e503eac..954b020473 100644
--- a/lib/models/repository-states/index.js
+++ b/lib/models/repository-states/index.js
@@ -1,6 +1,7 @@
export {expectedDelegates} from './state';
export {default as InitialState} from './loading';
+export {default as AbsentState} from './absent';
// Load and register remaining states
import './empty';
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index 6ea7313cbb..19250590ca 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -48,6 +48,11 @@ export default class State {
return false;
}
+ @shouldDelegate
+ isAbsent() {
+ return false;
+ }
+
@shouldDelegate
isPresent() {
return false;
diff --git a/lib/models/repository.js b/lib/models/repository.js
index e3dfba0e99..e5f7fca67d 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -5,10 +5,12 @@ import {Emitter} from 'event-kit';
import CompositeGitStrategy from '../composite-git-strategy';
import {readFile} from '../helpers';
import Remote, {nullRemote} from './remote';
-import {InitialState} from './repository-states';
+import {InitialState, AbsentState} from './repository-states';
const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
+const ABSENT_TOKEN = {};
+
export class AbortMergeError extends Error {
constructor(code, filePath) {
super();
@@ -35,18 +37,27 @@ export default class Repository {
this.emitter = new Emitter();
- this.loadPromise = new Promise(resolve => {
- const sub = this.onDidChangeState(() => {
- if (!this.isLoading()) {
- resolve();
- sub.dispose();
- } else if (this.isDestroyed()) {
- sub.dispose();
- }
+ if (workingDirectoryPath !== ABSENT_TOKEN) {
+ this.loadPromise = new Promise(resolve => {
+ const sub = this.onDidChangeState(() => {
+ if (!this.isLoading()) {
+ resolve();
+ sub.dispose();
+ } else if (this.isDestroyed()) {
+ sub.dispose();
+ }
+ });
});
- });
- this.transitionTo(InitialState);
+ this.transitionTo(InitialState);
+ } else {
+ this.loadPromise = Promise.reject(new Error('An absent repository will never load'));
+ this.transitionTo(AbsentState);
+ }
+ }
+
+ static absent() {
+ return new Repository(ABSENT_TOKEN);
}
// State management //////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index f765303896..cbbe505985 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -27,6 +27,25 @@ describe('Repository', function() {
assert.lengthOf(missing, 0);
});
+ describe('initial states', function() {
+ let repository;
+
+ afterEach(async function() {
+ repository && await repository.destroy();
+ });
+
+ it('begins in the Loading state with a working directory', async function() {
+ const workdir = await cloneRepository('three-files');
+ repository = new Repository(workdir);
+ assert.isTrue(repository.isInState('Loading'));
+ });
+
+ it('begins in the Absent state with .absent()', function() {
+ repository = Repository.absent();
+ assert.isTrue(repository.isInState('Absent'));
+ });
+ });
+
describe('init', function() {
it('creates a repository in the given dir and returns the repository', async function() {
const soonToBeRepositoryPath = fs.realpathSync(temp.mkdirSync());
From 30aa5aec6455408d0f4b1b69a91c7900575cee26 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:16:12 -0400
Subject: [PATCH 084/161] Use nullBranch and nullRemote in defaultProps
---
lib/controllers/github-tab-controller.js | 11 +++++----
lib/controllers/remote-pr-controller.js | 2 ++
.../repository-conflict-controller.js | 8 +++----
lib/controllers/status-bar-tile-controller.js | 17 ++++++++-----
lib/views/push-pull-menu-view.js | 24 ++++++++++---------
5 files changed, 35 insertions(+), 27 deletions(-)
diff --git a/lib/controllers/github-tab-controller.js b/lib/controllers/github-tab-controller.js
index 537b340222..27b0d119f2 100644
--- a/lib/controllers/github-tab-controller.js
+++ b/lib/controllers/github-tab-controller.js
@@ -5,6 +5,7 @@ import yubikiri from 'yubikiri';
import RemotePrController from './remote-pr-controller';
import GithubLoginModel from '../models/github-login-model';
+import {nullBranch} from '../models/branch';
import ObserveModelDecorator from '../decorators/observe-model';
import {RemotePropType, BranchPropType} from '../prop-types';
@@ -51,14 +52,14 @@ class RemoteSelector extends React.Component {
export default class GithubTabController extends React.Component {
static propTypes = {
repository: PropTypes.object,
- remotes: PropTypes.arrayOf(RemotePropType.isRequired),
- currentBranch: BranchPropType,
+ remotes: PropTypes.arrayOf(RemotePropType).isRequired,
+ currentBranch: BranchPropType.isRequired,
selectedRemoteName: PropTypes.string,
}
static defaultProps = {
- remotes: null,
- currentBranch: {name: '', isDetached: false},
+ remotes: [],
+ currentBranch: nullBranch,
selectedRemoteName: null,
}
@@ -68,7 +69,7 @@ export default class GithubTabController extends React.Component {
}
render() {
- if (!this.props.repository.isPresent() || this.props.remotes.length === 0) {
+ if (!this.props.repository.isPresent()) {
return null;
}
diff --git a/lib/controllers/remote-pr-controller.js b/lib/controllers/remote-pr-controller.js
index 5c2f64f38a..c2a2fd4acc 100644
--- a/lib/controllers/remote-pr-controller.js
+++ b/lib/controllers/remote-pr-controller.js
@@ -7,6 +7,7 @@ import {RemotePropType} from '../prop-types';
import ObserveModelDecorator from '../decorators/observe-model';
import GithubLoginView from '../views/github-login-view';
import {UNAUTHENTICATED} from '../models/github-login-model';
+import {nullRemote} from '../models/remote';
import PrInfoController from './pr-info-controller';
@ObserveModelDecorator({
@@ -31,6 +32,7 @@ export default class RemotePrController extends React.Component {
static defaultProps = {
host: 'https://api.github.com',
+ remote: nullRemote,
token: null,
}
diff --git a/lib/controllers/repository-conflict-controller.js b/lib/controllers/repository-conflict-controller.js
index 758b366fe0..530f8f114f 100644
--- a/lib/controllers/repository-conflict-controller.js
+++ b/lib/controllers/repository-conflict-controller.js
@@ -25,9 +25,8 @@ export default class RepositoryConflictController extends React.Component {
static propTypes = {
workspace: PropTypes.object.isRequired,
commandRegistry: PropTypes.object.isRequired,
- resolutionProgress: PropTypes.object,
- repository: PropTypes.object,
- workingDirectoryPath: PropTypes.string,
+ resolutionProgress: PropTypes.object.isRequired,
+ repository: PropTypes.object.isRequired,
mergeConflictPaths: PropTypes.arrayOf(PropTypes.string),
isRebasing: PropTypes.bool,
refreshResolutionProgress: PropTypes.func,
@@ -76,14 +75,13 @@ export default class RepositoryConflictController extends React.Component {
getConflictingEditors() {
if (
- !this.props.workingDirectoryPath ||
this.props.mergeConflictPaths.length === 0 ||
this.state.openEditors.length === 0
) {
return [];
}
- const commonBasePath = this.props.workingDirectoryPath;
+ const commonBasePath = this.props.repository.getWorkingDirectoryPath();
const fullMergeConflictPaths = new Set(
this.props.mergeConflictPaths.map(relativePath => path.join(commonBasePath, relativePath)),
);
diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js
index f41f1a2539..d710400ed7 100644
--- a/lib/controllers/status-bar-tile-controller.js
+++ b/lib/controllers/status-bar-tile-controller.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ObserveModelDecorator from '../decorators/observe-model';
-import {BranchPropType} from '../prop-types';
+import {BranchPropType, RemotePropType} from '../prop-types';
import BranchView from '../views/branch-view';
import BranchMenuView from '../views/branch-menu-view';
import PushPullView from '../views/push-pull-view';
@@ -10,6 +10,8 @@ import PushPullMenuView from '../views/push-pull-menu-view';
import ChangedFilesCountView from '../views/changed-files-count-view';
import Tooltip from '../views/tooltip';
import Commands, {Command} from '../views/commands';
+import {nullBranch} from '../models/branch';
+import {nullRemote} from '../models/remote';
import yubikiri from 'yubikiri';
import {autobind} from 'core-decorators';
@@ -35,7 +37,7 @@ import {autobind} from 'core-decorators';
return changedFiles.size;
}),
- remoteName: async query => repository.getRemoteForBranch((await query.currentBranch).getName()),
+ currentRemote: async query => repository.getRemoteForBranch((await query.currentBranch).getName()),
aheadCount: async query => repository.getAheadCount((await query.currentBranch).getName()),
behindCount: async query => repository.getBehindCount((await query.currentBranch).getName()),
});
@@ -50,7 +52,7 @@ export default class StatusBarTileController extends React.Component {
repository: PropTypes.object.isRequired,
currentBranch: BranchPropType.isRequired,
branches: PropTypes.arrayOf(BranchPropType).isRequired,
- remoteName: PropTypes.string,
+ currentRemote: RemotePropType.isRequired,
aheadCount: PropTypes.number,
behindCount: PropTypes.number,
changedFilesCount: PropTypes.number,
@@ -58,6 +60,9 @@ export default class StatusBarTileController extends React.Component {
}
static defaultProps = {
+ currentBranch: nullBranch,
+ branches: [],
+ currentRemote: nullRemote,
toggleGitPanel: () => {},
}
@@ -75,7 +80,7 @@ export default class StatusBarTileController extends React.Component {
const repoProps = {
currentBranch: this.props.currentBranch,
branches: this.props.branches,
- remoteName: this.props.remoteName,
+ currentRemote: this.props.currentRemote,
aheadCount: this.props.aheadCount,
behindCount: this.props.behindCount,
changedFilesCount: this.props.changedFilesCount,
@@ -88,11 +93,11 @@ export default class StatusBarTileController extends React.Component {
this.push({force: false, setUpstream: !this.props.remoteName})}
+ callback={() => this.push({force: false, setUpstream: !this.props.currentRemote.isPresent()})}
/>
this.push({force: true, setUpstream: !this.props.remoteName})}
+ callback={() => this.push({force: true, setUpstream: !this.props.currentRemote.isPresent()})}
/>
@@ -103,7 +105,7 @@ export default class PushPullMenuView extends React.Component {
return 'Note: you are not on a branch. Please create one if you wish to push your work anywhere.';
}
- if (!this.props.remoteName) {
+ if (!this.props.currentRemote.isPresent()) {
return `Note: No remote detected for branch ${this.props.currentBranch.getName()}. ` +
'Pushing will set up a remote tracking branch on remote repo "origin"';
}
@@ -162,7 +164,7 @@ export default class PushPullMenuView extends React.Component {
@autobind
async push() {
await this.attemptGitOperation(
- () => this.props.push({force: this.state.force, setUpstream: !this.props.remoteName}),
+ () => this.props.push({force: this.state.force, setUpstream: !this.props.currentRemote.isPresent()}),
description => {
if (/rejected[\s\S]*failed to push/.test(description)) {
return {
From 5190d6386729aa031199f6a697a6d986cd4387ed Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:16:30 -0400
Subject: [PATCH 085/161] Async tempdir helper
---
lib/helpers.js | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/lib/helpers.js b/lib/helpers.js
index c43a51943d..09cd14d0d8 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -1,5 +1,6 @@
import path from 'path';
import fs from 'fs-extra';
+import temp from 'temp';
import {ncp} from 'ncp';
export function getPackageRoot() {
@@ -149,10 +150,15 @@ export function copyFile(source, target) {
});
}
-export function getTempDir(prefix) {
+export function getTempDir(prefix = '') {
return new Promise((resolve, reject) => {
- fs.mkdtemp(prefix, (err, folder) => {
- if (err) { return reject(err); } else { return resolve(folder); }
+ temp.mkdir(prefix, (tempError, folder) => {
+ if (tempError) {
+ reject(tempError);
+ return;
+ }
+
+ fs.realpath(folder, (realError, rpath) => (realError ? reject(realError) : resolve(rpath)));
});
});
}
From 778183e5700e03b99d86e8045397f65e898911b8 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:16:50 -0400
Subject: [PATCH 086/161] Branch and Remote polish
---
lib/models/branch.js | 4 ++--
lib/models/remote.js | 24 ++++++++++++++++++++++++
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/lib/models/branch.js b/lib/models/branch.js
index 3d2aecd3ea..6f6493ad6f 100644
--- a/lib/models/branch.js
+++ b/lib/models/branch.js
@@ -3,7 +3,7 @@ const DETACHED = {};
export default class Branch {
constructor(name, detached = null) {
this.name = name;
- this.isDetached = detached === DETACHED;
+ this.detached = detached === DETACHED;
}
static createDetached(describe) {
@@ -15,7 +15,7 @@ export default class Branch {
}
isDetached() {
- return this.isDetached;
+ return this.detached;
}
isPresent() {
diff --git a/lib/models/remote.js b/lib/models/remote.js
index b802449e88..e272d62767 100644
--- a/lib/models/remote.js
+++ b/lib/models/remote.js
@@ -39,6 +39,14 @@ export default class Remote {
}
function githubInfoFromRemote(remoteUrl) {
+ if (!remoteUrl) {
+ return {
+ isGithubRepo: false,
+ owner: null,
+ repo: null,
+ };
+ }
+
// proto login domain owner repo
const regex = /(?:.+:\/\/)?(?:.+@)?github\.com[:/]([^/]+)\/(.+)/;
const match = remoteUrl.match(regex);
@@ -62,6 +70,22 @@ export const nullRemote = {
return '';
},
+ getUrl() {
+ return '';
+ },
+
+ isGithubRepo() {
+ return false;
+ },
+
+ getOwner() {
+ return null;
+ },
+
+ getRepo() {
+ return null;
+ },
+
getNameOr(fallback) {
return fallback;
},
From 5794042ee2ea0155dbe2cb3809ff35fa3b93cec3 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:17:03 -0400
Subject: [PATCH 087/161] Return the `transitionTo` result promise
---
lib/models/repository-states/loading.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js
index 4214c6e96d..1b9ea13015 100644
--- a/lib/models/repository-states/loading.js
+++ b/lib/models/repository-states/loading.js
@@ -4,9 +4,9 @@ export default class Loading extends State {
async start() {
if (await this.git().isGitRepository()) {
const history = await this.loadHistoryPayload();
- this.transitionTo('Present', history);
+ return this.transitionTo('Present', history);
} else {
- this.transitionTo('Empty');
+ return this.transitionTo('Empty');
}
}
From 1b3364ecbce483e70f64d67d52a89d0cba56abf2 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:17:17 -0400
Subject: [PATCH 088/161] Clear the cache on operation failure too
---
lib/models/repository-states/present.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index b93ab659cc..1301d0a153 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -53,6 +53,7 @@ function invalidate() {
return result;
},
err => {
+ cache.clear();
return Promise.reject(err);
},
);
From e75eb354c03c082ad0599e8de68d2970b3d40451 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:17:31 -0400
Subject: [PATCH 089/161] Return Branch array from getBranches()
---
lib/models/repository-states/present.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index 1301d0a153..be44c1b0a7 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -337,8 +337,9 @@ export default class Present extends State {
// Branches
getBranches() {
- return this.cache.getOrSet('branches', () => {
- return this.git().getBranches();
+ return this.cache.getOrSet('branches', async () => {
+ const branchNames = await this.git().getBranches();
+ return branchNames.map(branchName => new Branch(branchName));
});
}
From fb27202b20c240cc155aaf376beb8a3b43264ed5 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:17:43 -0400
Subject: [PATCH 090/161] Delegate `.refresh()` back to Repository
---
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 19250590ca..9330ae0416 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -88,7 +88,7 @@ export default class State {
@shouldDelegate
observeFilesystemChange() {
- this.refresh();
+ this.repository.refresh();
}
// Git operations ////////////////////////////////////////////////////////////////////////////////////////////////////
From 724285d09486295da6ca994798e89eeca0f42cab Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:18:05 -0400
Subject: [PATCH 091/161] Make nullWorkdirContext a singleton
---
lib/models/workdir-context-pool.js | 8 +++---
lib/models/workdir-context.js | 41 +++++++++++++++++-------------
2 files changed, 28 insertions(+), 21 deletions(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index c513764250..c5db10fac9 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -1,7 +1,7 @@
import compareSets from 'compare-sets';
import {Emitter, CompositeDisposable} from 'event-kit';
-import WorkdirContext, {NullWorkdirContext} from './workdir-context';
+import WorkdirContext, {nullWorkdirContext} from './workdir-context';
/**
* Manage a WorkdirContext for each open directory.
@@ -26,12 +26,12 @@ export default class WorkdirContextPool {
* Access the context mapped to a known directory.
*/
getContext(directory) {
- return this.contexts.get(directory) || new NullWorkdirContext(directory);
+ return this.contexts.get(directory) || nullWorkdirContext;
}
add(directory, options = {}) {
if (this.contexts.has(directory)) {
- return;
+ return this.getContext(directory);
}
let savedResolutionProgress = {};
@@ -60,6 +60,8 @@ export default class WorkdirContextPool {
disposable.dispose();
}
}));
+
+ return context;
}
replace(directory, options = {}) {
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 737daa4ded..379d95e2e0 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -6,7 +6,7 @@ import ResolutionProgress from './conflicts/resolution-progress';
import FileSystemChangeObserver from './file-system-change-observer';
import WorkspaceChangeObserver from './workspace-change-observer';
-const DORMANT = {};
+const ABSENT = {};
/*
* Bundle of model objects associated with a git working directory.
@@ -20,22 +20,27 @@ export default class WorkdirContext {
/*
* Available options:
- * - `options.repository`: Pre-initialized Repository instance if one is available.
* - `options.savedResolutionProgress`: Serialized state used to initialize the ResolutionProgress.
* - `options.window`: Browser window global, used on Linux by the WorkspaceChangeObserver.
* - `options.workspace`: Atom's workspace singleton, used on Linux by the WorkspaceChangeObserver.
* - `options.promptCallback`: Callback used to collect information interactively through Atom.
*/
constructor(directory, options = {}) {
- this.directory = directory;
- const {repository, window: theWindow, workspace, promptCallback} = options;
+ if (directory !== ABSENT) {
+ this.directory = directory;
+ this.repository = new Repository(directory);
+ } else {
+ this.directory = null;
+ this.repository = Repository.absent();
+ }
+
+ const {window: theWindow, workspace, promptCallback} = options;
this.savedResolutionProgress = options.savedResolutionProgress || {};
this.destroyed = false;
this.emitter = new Emitter();
this.subs = new CompositeDisposable();
- this.repository = repository || new Repository(directory);
this.observer = process.platform === 'linux'
? new WorkspaceChangeObserver(theWindow, workspace, this.repository)
: new FileSystemChangeObserver(this.repository);
@@ -45,17 +50,13 @@ export default class WorkdirContext {
this.repository.setPromptCallback(promptCallback);
}
- if (options.dormant !== DORMANT) {
- // Wire up event forwarding among models
- this.subs.add(this.repository.onDidChangeState(this.repositoryChangedState));
- this.subs.add(this.observer.onDidChange(payload => this.repository.observeFilesystemChange(payload)));
- this.subs.add(this.observer.onDidChangeWorkdirOrHead(() => this.emitter.emit('did-change-workdir-or-head')));
+ // Wire up event forwarding among models
+ this.subs.add(this.repository.onDidChangeState(this.repositoryChangedState));
+ this.subs.add(this.observer.onDidChange(payload => this.repository.observeFilesystemChange(payload)));
+ this.subs.add(this.observer.onDidChangeWorkdirOrHead(() => this.emitter.emit('did-change-workdir-or-head')));
- // If a pre-loaded Repository was provided, broadcast an initial state change event.
- if (this.repository) {
- this.repositoryChangedState({from: null, to: this.repository.state});
- }
- }
+ // If a pre-loaded Repository was provided, broadcast an initial state change event.
+ this.repositoryChangedState({from: null, to: this.repository.state});
}
/**
@@ -78,6 +79,8 @@ export default class WorkdirContext {
const head = await this.repository.getLastCommit();
this.resolutionProgress.load(head.getSha(), this.savedResolutionProgress);
} else if (this.repository.isDestroyed()) {
+ this.emitter.emit('did-destroy-repository');
+
this.observer.destroy();
}
@@ -175,12 +178,14 @@ export default class WorkdirContext {
}
}
-export class NullWorkdirContext extends WorkdirContext {
- constructor(directory) {
- super(directory, {dormant: DORMANT});
+class NullWorkdirContext extends WorkdirContext {
+ constructor() {
+ super(ABSENT);
}
isPresent() {
return false;
}
}
+
+export const nullWorkdirContext = new NullWorkdirContext();
From a73451918438914f53cdafa547ce9572e8fc1dff Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:18:20 -0400
Subject: [PATCH 092/161] Handle absent Repositories
---
lib/models/repository.js | 32 ++++++++++++++------------------
1 file changed, 14 insertions(+), 18 deletions(-)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index e5f7fca67d..1ad962de08 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -9,7 +9,7 @@ import {InitialState, AbsentState} from './repository-states';
const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
-const ABSENT_TOKEN = {};
+const ABSENT = {};
export class AbortMergeError extends Error {
constructor(code, filePath) {
@@ -37,27 +37,22 @@ export default class Repository {
this.emitter = new Emitter();
- if (workingDirectoryPath !== ABSENT_TOKEN) {
- this.loadPromise = new Promise(resolve => {
- const sub = this.onDidChangeState(() => {
- if (!this.isLoading()) {
- resolve();
- sub.dispose();
- } else if (this.isDestroyed()) {
- sub.dispose();
- }
- });
+ this.loadPromise = new Promise(resolve => {
+ const sub = this.onDidChangeState(() => {
+ if (!this.isLoading()) {
+ resolve();
+ sub.dispose();
+ } else if (this.isDestroyed()) {
+ sub.dispose();
+ }
});
+ });
- this.transitionTo(InitialState);
- } else {
- this.loadPromise = Promise.reject(new Error('An absent repository will never load'));
- this.transitionTo(AbsentState);
- }
+ this.transitionTo(workingDirectoryPath !== ABSENT ? InitialState : AbsentState);
}
static absent() {
- return new Repository(ABSENT_TOKEN);
+ return new Repository(ABSENT);
}
// State management //////////////////////////////////////////////////////////////////////////////////////////////////
@@ -76,7 +71,7 @@ export default class Repository {
}
getLoadPromise() {
- return this.loadPromise;
+ return this.isAbsent() ? this.loadPromise : Promise.reject(new Error('An absent repository will never load'));
}
/*
@@ -176,6 +171,7 @@ export default class Repository {
const delegates = [
'isLoading',
'isEmpty',
+ 'isAbsent',
'isPresent',
'isDestroyed',
From d575d686b1ef78bbf06b9baee8582fe5ec271a0d Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:49:12 -0400
Subject: [PATCH 093/161] Simplify WorkdirContext tests for synchronous models
---
test/models/workdir-context.test.js | 161 ++++++++++------------------
1 file changed, 59 insertions(+), 102 deletions(-)
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 070593f6ce..07748c599d 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -3,10 +3,9 @@ import {CompositeDisposable, Disposable} from 'event-kit';
import {cloneRepository, sha} from '../helpers';
import WorkdirContext from '../../lib/models/workdir-context';
-import Repository from '../../lib/models/repository';
describe('WorkdirContext', function() {
- let context, workingDirectory, subs, options;
+ let context, workingDirectory, subs;
let mockPromptCallback;
beforeEach(async function() {
@@ -32,12 +31,12 @@ describe('WorkdirContext', function() {
mockPromptCallback = query => 'reply';
- options = {
+ context = new WorkdirContext(workingDirectory, {
window: mockWindow,
workspace: mockWorkspace,
promptCallback: mockPromptCallback,
savedResolutionProgress: mockSavedResolutionProgress,
- };
+ });
});
afterEach(async function() {
@@ -45,124 +44,82 @@ describe('WorkdirContext', function() {
subs.dispose();
});
- describe('without an initial Repository', function() {
- beforeEach(function() {
- context = new WorkdirContext(workingDirectory, options);
- });
-
- it('returns synchronous models in their initial states', function() {
- assert.isTrue(context.getRepository().isLoading());
- assert.isTrue(context.getResolutionProgress().isEmpty());
- assert.isFalse(context.getChangeObserver().isStarted());
- });
-
- it('updates the resolution progress once the repository loads', async function() {
- const progress = context.getResolutionProgress();
-
- await context.getRepositoryStatePromise('Present');
-
- assert.isFalse(progress.isEmpty());
- assert.equal(progress.getRemaining('a.txt'), 2);
- assert.equal(progress.getRemaining('b.txt'), 3);
- });
-
- it('starts the change observer after the repository loads', async function() {
- const observer = context.getChangeObserver();
- await context.getObserverStartedPromise();
- assert.isTrue(observer.isStarted());
- });
-
- it('configures the repository with a prompt callback', async function() {
- const repo = context.getRepository();
- await repo.getLoadPromise();
-
- for (const strategy of repo.git.getImplementers()) {
- assert.strictEqual(strategy.prompt, mockPromptCallback);
- }
- });
-
- it('notifies the repository on any filesystem change', async function() {
- const repo = context.getRepository();
- await context.getRepositoryStatePromise('Present');
+ it('returns synchronous models in their initial states', function() {
+ assert.isTrue(context.getRepository().isLoading());
+ assert.isTrue(context.getResolutionProgress().isEmpty());
+ assert.isFalse(context.getChangeObserver().isStarted());
+ });
- sinon.spy(repo, 'observeFilesystemChange');
+ it('updates the resolution progress once the repository loads', async function() {
+ const progress = context.getResolutionProgress();
- const payload = {};
- context.getChangeObserver().didChange(payload);
+ await context.getRepositoryStatePromise('Present');
- assert.isTrue(repo.observeFilesystemChange.calledWith(payload));
- });
+ assert.isFalse(progress.isEmpty());
+ assert.equal(progress.getRemaining('a.txt'), 2);
+ assert.equal(progress.getRemaining('b.txt'), 3);
+ });
- it('re-emits an event on workdir or head change', async function() {
- const listener = sinon.spy();
- subs.add(context.onDidChangeWorkdirOrHead(listener));
+ it('starts the change observer after the repository loads', async function() {
+ const observer = context.getChangeObserver();
+ await context.getObserverStartedPromise();
+ assert.isTrue(observer.isStarted());
+ });
- await context.getRepositoryStatePromise('Present');
+ it('configures the repository with a prompt callback', async function() {
+ const repo = context.getRepository();
+ await repo.getLoadPromise();
- context.getChangeObserver().didChangeWorkdirOrHead();
- assert.isTrue(listener.called);
- });
+ for (const strategy of repo.git.getImplementers()) {
+ assert.strictEqual(strategy.prompt, mockPromptCallback);
+ }
+ });
- it('destroys the repository on destroy()', async function() {
- await context.getRepositoryStatePromise('Present');
- const repo = context.getRepository();
+ it('notifies the repository on any filesystem change', async function() {
+ const repo = context.getRepository();
+ await context.getRepositoryStatePromise('Present');
- await context.destroy();
- assert.isTrue(repo.isDestroyed());
- });
+ sinon.spy(repo, 'observeFilesystemChange');
- it('stops the change observer on destroy()', async function() {
- const observer = context.getChangeObserver();
- await observer.start();
+ const payload = {};
+ context.getChangeObserver().didChange(payload);
- await context.destroy();
- assert.isFalse(observer.isStarted());
- });
+ assert.isTrue(repo.observeFilesystemChange.calledWith(payload));
+ });
- it('can be destroyed twice', async function() {
- assert.isFalse(context.isDestroyed());
+ it('re-emits an event on workdir or head change', async function() {
+ const listener = sinon.spy();
+ subs.add(context.onDidChangeWorkdirOrHead(listener));
- await context.destroy();
- assert.isTrue(context.isDestroyed());
+ await context.getRepositoryStatePromise('Present');
- await context.destroy();
- assert.isTrue(context.isDestroyed());
- });
+ context.getChangeObserver().didChangeWorkdirOrHead();
+ assert.isTrue(listener.called);
});
- describe('with an initial Repository', function() {
- let repository;
+ it('destroys the repository on destroy()', async function() {
+ await context.getRepositoryStatePromise('Present');
+ const repo = context.getRepository();
- beforeEach(async function() {
- repository = new Repository(workingDirectory);
- await repository.getLoadPromise();
- sinon.spy(repository, 'getLastCommit');
- options.repository = repository;
-
- context = new WorkdirContext(workingDirectory, options);
- });
-
- it('returns the repository synchronously', function() {
- assert.strictEqual(repository, context.getRepository());
- });
+ await context.destroy();
+ assert.isTrue(repo.isDestroyed());
+ });
- it('begins populating the resolution progress immediately', async function() {
- assert.isTrue(repository.getLastCommit.called);
+ it('stops the change observer on destroy()', async function() {
+ const observer = context.getChangeObserver();
+ await observer.start();
- await context.getRepositoryStatePromise('Present');
+ await context.destroy();
+ assert.isFalse(observer.isStarted());
+ });
- const progress = context.getResolutionProgress();
- assert.isFalse(progress.isEmpty());
- assert.equal(progress.getRemaining('a.txt'), 2);
- assert.equal(progress.getRemaining('b.txt'), 3);
- });
+ it('can be destroyed twice', async function() {
+ assert.isFalse(context.isDestroyed());
- it('starts the change observer automatically', async function() {
- const observer = context.getChangeObserver();
- assert.isFalse(observer.isStarted());
+ await context.destroy();
+ assert.isTrue(context.isDestroyed());
- await context.getObserverStartedPromise();
- assert.isTrue(observer.isStarted());
- });
+ await context.destroy();
+ assert.isTrue(context.isDestroyed());
});
});
From dd88f6f2f99a38d1c648bb8c8234d9688b21eaac Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:49:50 -0400
Subject: [PATCH 094/161] Use the nullWorkdirContext singleton
---
lib/github-package.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 0071bfdd86..abf9fab748 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -7,7 +7,7 @@ import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
import WorkdirCache from './models/workdir-cache';
-import {NullWorkdirContext} from './models/workdir-context';
+import {nullWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import StyleCalculator from './models/style-calculator';
@@ -36,7 +36,7 @@ export default class GithubPackage {
this.confirm = confirm;
this.activeContextQueue = new AsyncQueue();
- this.activeContext = new NullWorkdirContext(null);
+ this.activeContext = nullWorkdirContext;
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
@@ -57,7 +57,7 @@ export default class GithubPackage {
}),
this.contextPool.onDidDestroyRepository(context => {
if (context === this.activeContext) {
- this.setActiveContext(new NullWorkdirContext(context.getWorkingDirectory()));
+ this.setActiveContext(nullWorkdirContext);
}
}),
);
From ea1000b89fc0a728c2c98abf8e58ee2597e5d5b1 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:50:05 -0400
Subject: [PATCH 095/161] No need to .rerender() on repository load
---
lib/github-package.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index abf9fab748..c859dd5dd4 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -51,7 +51,6 @@ export default class GithubPackage {
this.contextPool.onDidChangeWorkdirOrHead(context => {
this.refreshAtomGitRepository(context.getWorkingDirectory());
}),
- this.contextPool.onDidLoadRepository(context => context === this.activeContext && this.rerender()),
this.contextPool.onDidUpdateRepository(context => {
this.switchboard.didUpdateRepository(context.getRepository());
}),
From b7937fdc818a666f511d38651d6495950b0d75e9 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:50:16 -0400
Subject: [PATCH 096/161] :fire: unused Disposable cleanup
---
lib/github-package.js | 5 -----
1 file changed, 5 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index c859dd5dd4..dbe16359da 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -194,12 +194,7 @@ export default class GithubPackage {
async deactivate() {
this.subscriptions.dispose();
-
- this.destroyedRepositorySubscription && this.destroyedRepositorySubscription.dispose();
- this.changedRepositorySubscription && this.changedRepositorySubscription.dispose();
-
this.contextPool.clear();
-
await yardstick.flush();
}
From 51320c7796df08a67f8c7b7351ee5094900a7f38 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:50:42 -0400
Subject: [PATCH 097/161] Clone and init work with empty Repositories
---
lib/github-package.js | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index dbe16359da..c1751a94bf 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -216,8 +216,9 @@ export default class GithubPackage {
if (!projectPath) {
throw new Error('Must specify a project path to create a repository for');
}
- const repository = await Repository.init(projectPath);
- this.contextPool.replace(projectPath, {repository});
+
+ const repository = this.contextPool.add(projectPath).getRepository();
+ await repository.init();
this.workdirCache.invalidate(projectPath);
await this.scheduleActiveContextUpdate();
@@ -225,8 +226,10 @@ export default class GithubPackage {
@autobind
async cloneRepositoryForProjectPath(remoteUrl, projectPath) {
- const repository = await Repository.clone(remoteUrl, projectPath);
- this.contextPool.replace(projectPath, {repository});
+ const context = this.contextPool.getContext(projectPath);
+ const repository = context.isPresent() ? context.getRepository() : new Repository(projectPath);
+
+ await repository.clone(remoteUrl);
this.workdirCache.invalidate(projectPath);
this.project.addPath(projectPath);
From 818ea8314353c263dc63e300b5b6e7b9f166ed2f Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:51:04 -0400
Subject: [PATCH 098/161] Always include project path contexts, even if they
are empty
---
lib/github-package.js | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index c1751a94bf..8759dc9a68 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -272,17 +272,23 @@ export default class GithubPackage {
* projects and pane items.
*/
async getNextContext(savedState) {
- const candidates = new Set(this.project.getPaths());
+ const workdirs = new Set(
+ await Promise.all(
+ this.project.getPaths().map(async projectPath => {
+ const workdir = await this.workdirCache.find(projectPath);
+ return workdir || projectPath;
+ }),
+ ),
+ );
const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
- if (activeItemPath) {
- candidates.add(activeItemPath);
+ if (activeItemPath && !this.project.contains(activeItemPath)) {
+ const activeItemWorkdir = await this.workdirCache.find(activeItemPath);
+ if (activeItemWorkdir) {
+ workdirs.add(activeItemWorkdir);
+ }
}
- const workdirs = await Promise.all(
- Array.from(candidates, candidate => this.workdirCache.find(candidate)),
- );
-
this.contextPool.set(workdirs, savedState);
if (activeItemPath) {
From 9bcbc98d13dc69d86f093644a73487ef0c6dfd47 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:51:16 -0400
Subject: [PATCH 099/161] Switchboard accessor for clarify in tests
---
lib/github-package.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/lib/github-package.js b/lib/github-package.js
index 8759dc9a68..ff76529c2c 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -231,6 +231,7 @@ export default class GithubPackage {
await repository.clone(remoteUrl);
this.workdirCache.invalidate(projectPath);
+
this.project.addPath(projectPath);
await this.scheduleActiveContextUpdate();
@@ -252,6 +253,10 @@ export default class GithubPackage {
return this.contextPool;
}
+ getSwitchboard() {
+ return this.switchboard;
+ }
+
@autobind
async scheduleActiveContextUpdate(savedState = {}) {
this.switchboard.didScheduleActiveContextUpdate();
From 3327006cd66f1288e026b66392a8dd7963eb3f8d Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:52:05 -0400
Subject: [PATCH 100/161] Await the right things in GithubPackage tests
---
test/github-package.test.js | 166 +++++++++++++++++++++---------------
1 file changed, 97 insertions(+), 69 deletions(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index d52d5a46e2..097371aa1a 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -4,7 +4,7 @@ import temp from 'temp';
import until from 'test-until';
import {cloneRepository} from './helpers';
-import {writeFile} from '../lib/helpers';
+import {writeFile, getTempDir} from '../lib/helpers';
import GithubPackage from '../lib/github-package';
describe('GithubPackage', function() {
@@ -33,30 +33,23 @@ describe('GithubPackage', function() {
atomEnv.destroy();
});
- function untilWorkdirs(...workdirs) {
- return until(`working directories ${workdirs.join(', ')} are loaded`, () => {
- return workdirs.every(workdir => contextPool.getContext(workdir).isPresent());
- });
- }
-
- function untilRepositories(...workdirs) {
- return untilWorkdirs(...workdirs).then(() => Promise.all(
- workdirs.map(workdir => contextPool.getContext(workdir).getRepositoryPromise()),
- ));
- }
-
describe('activate()', function() {
it('uses models from preexisting projects', async function() {
- const [workdirPath1, workdirPath2] = await Promise.all([
+ const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([
cloneRepository('three-files'),
cloneRepository('three-files'),
+ getTempDir(),
]);
- project.setPaths([workdirPath1, workdirPath2]);
+ project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
+ assert.isTrue(contextPool.getContext(workdirPath1).isPresent());
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
+ assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent());
- await untilRepositories(workdirPath1, workdirPath2);
- assert.isNull(githubPackage.getActiveRepository());
+ assert.isTrue(githubPackage.getActiveRepository().isAbsent());
});
it('uses an active model from a single preexisting project', async function() {
@@ -64,8 +57,13 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath]);
await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.isNotNull(githubPackage.getActiveRepository());
+ const context = contextPool.getContext(workdirPath);
+ assert.isTrue(context.isPresent());
+
+ assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository());
+ assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress());
assert.equal(githubPackage.getActiveWorkdir(), workdirPath);
});
@@ -78,8 +76,12 @@ describe('GithubPackage', function() {
await workspace.open(path.join(workdirPath2, 'a.txt'));
await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.isNotNull(githubPackage.getActiveRepository());
+ const context = contextPool.getContext(workdirPath2);
+ assert.isTrue(context.isPresent());
+ assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository());
+ assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress());
assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
@@ -94,8 +96,12 @@ describe('GithubPackage', function() {
await githubPackage.activate({
activeRepositoryPath: workdirPath2,
});
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.isNotNull(githubPackage.getActiveRepository());
+ const context = contextPool.getContext(workdirPath2);
+ assert.isTrue(context.isPresent());
+ assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository());
+ assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress());
assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
@@ -110,8 +116,12 @@ describe('GithubPackage', function() {
await githubPackage.activate({
activeRepositoryPath: workdirPath1,
});
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.isNotNull(githubPackage.getActiveRepository());
+ const context = contextPool.getContext(workdirPath2);
+ assert.isTrue(context.isPresent());
+ assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository());
+ assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress());
assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
@@ -125,8 +135,12 @@ describe('GithubPackage', function() {
await githubPackage.activate({
activeRepositoryPath: workdirPath2,
});
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.isNotNull(githubPackage.getActiveRepository());
+ const context = contextPool.getContext(workdirPath1);
+ assert.isTrue(context.isPresent());
+ assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository());
+ assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress());
assert.equal(githubPackage.getActiveWorkdir(), workdirPath1);
});
@@ -139,12 +153,13 @@ describe('GithubPackage', function() {
project.setPaths([workdirMergeConflict, workdirNoConflict, nonRepositoryPath]);
await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
// Open a file in the merge conflict repository.
await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-ours.txt'));
await githubPackage.scheduleActiveContextUpdate();
- const resolutionMergeConflict = await contextPool.getContext(workdirMergeConflict).getResolutionProgressPromise();
+ const resolutionMergeConflict = contextPool.getContext(workdirMergeConflict).getResolutionProgress();
await assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionMergeConflict);
// Record some resolution progress to recall later
@@ -154,7 +169,7 @@ describe('GithubPackage', function() {
await workspace.open(path.join(workdirNoConflict, 'b.txt'));
await githubPackage.scheduleActiveContextUpdate();
- const resolutionNoConflict = await contextPool.getContext(workdirNoConflict).getResolutionProgressPromise();
+ const resolutionNoConflict = contextPool.getContext(workdirNoConflict).getResolutionProgress();
assert.strictEqual(githubPackage.getActiveResolutionProgress(), resolutionNoConflict);
assert.isTrue(githubPackage.getActiveResolutionProgress().isEmpty());
@@ -162,7 +177,7 @@ describe('GithubPackage', function() {
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
await githubPackage.scheduleActiveContextUpdate();
- assert.isNull(githubPackage.getActiveResolutionProgress());
+ assert.isTrue(githubPackage.getActiveResolutionProgress().isEmpty());
// Re-open a file in the merge conflict repository.
await workspace.open(path.join(workdirMergeConflict, 'modified-on-both-theirs.txt'));
@@ -184,10 +199,18 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
- await untilWorkdirs(workdirPath1, workdirPath2);
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
+ assert.isTrue(contextPool.getContext(workdirPath1).isPresent());
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
+ assert.isFalse(contextPool.getContext(workdirPath3).isPresent());
project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
- await untilWorkdirs(workdirPath1, workdirPath2, workdirPath3);
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
+ assert.isTrue(contextPool.getContext(workdirPath1).isPresent());
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
+ assert.isTrue(contextPool.getContext(workdirPath3).isPresent());
});
it('destroys contexts associated with the removed project folders', async function() {
@@ -198,13 +221,11 @@ describe('GithubPackage', function() {
]);
project.setPaths([workdirPath1, workdirPath2, workdirPath3]);
await githubPackage.activate();
- await untilRepositories(workdirPath1, workdirPath2, workdirPath3);
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- const [repository1, repository2, repository3] = await Promise.all(
- [workdirPath1, workdirPath2, workdirPath3].map(workdir => {
- return contextPool.getContext(workdir).getRepositoryPromise();
- }),
- );
+ const [repository1, repository2, repository3] = [workdirPath1, workdirPath2, workdirPath3].map(workdir => {
+ return contextPool.getContext(workdir).getRepository();
+ });
sinon.stub(repository1, 'destroy');
sinon.stub(repository2, 'destroy');
@@ -212,9 +233,10 @@ describe('GithubPackage', function() {
project.removePath(workdirPath1);
project.removePath(workdirPath3);
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.equal(repository1.destroy.callCount, 1);
- await assert.async.equal(repository3.destroy.callCount, 1);
+ assert.equal(repository1.destroy.callCount, 1);
+ assert.equal(repository3.destroy.callCount, 1);
assert.isFalse(repository2.destroy.called);
assert.isFalse(contextPool.getContext(workdirPath1).isPresent());
@@ -231,14 +253,14 @@ describe('GithubPackage', function() {
]);
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
- await untilRepositories(workdirPath1, workdirPath2);
const repository2 = contextPool.getContext(workdirPath2).getRepository();
- assert.isNull(githubPackage.getActiveRepository());
+ assert.isTrue(githubPackage.getActiveRepository().isAbsent());
- await workspace.open(path.join(workdirPath2, 'b.txt'));
+ workspace.open(path.join(workdirPath2, 'b.txt'));
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await assert.async.strictEqual(githubPackage.getActiveRepository(), repository2);
+ assert.strictEqual(githubPackage.getActiveRepository(), repository2);
});
it('adds a new context if not in a project', async function() {
@@ -247,12 +269,17 @@ describe('GithubPackage', function() {
cloneRepository('three-files'),
]);
project.setPaths([workdirPath1]);
+
await githubPackage.activate();
- await untilRepositories(workdirPath1);
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- await workspace.open(path.join(workdirPath2, 'c.txt'));
+ assert.isFalse(contextPool.getContext(workdirPath2).isPresent());
- await assert.async.equal(githubPackage.getActiveWorkdir(), workdirPath2);
+ workspace.open(path.join(workdirPath2, 'c.txt'));
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
+ assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
});
it('removes a context if not in a project', async function() {
@@ -262,12 +289,13 @@ describe('GithubPackage', function() {
]);
project.setPaths([workdirPath1]);
await githubPackage.activate();
- await workspace.open(path.join(workdirPath2, 'c.txt'));
- await untilRepositories(workdirPath1, workdirPath2);
+
+ workspace.open(path.join(workdirPath2, 'c.txt'));
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
workspace.getActivePane().destroyActiveItem();
- await assert.async.isFalse(contextPool.getContext(workdirPath2).isPresent());
+ assert.isFalse(contextPool.getContext(workdirPath2).isPresent());
});
});
@@ -302,7 +330,7 @@ describe('GithubPackage', function() {
await githubPackage.scheduleActiveContextUpdate();
- assert.isNull(githubPackage.getActiveRepository());
+ assert.isTrue(githubPackage.getActiveRepository().isAbsent());
});
it('uses the context of a single open project', async function() {
@@ -325,7 +353,8 @@ describe('GithubPackage', function() {
await githubPackage.scheduleActiveContextUpdate();
- assert.isNull(githubPackage.getActiveRepository());
+ assert.isFalse(contextPool.getContext(nonRepositoryPath).isPresent());
+ assert.isTrue(githubPackage.getActiveRepository().isEmpty());
});
it('activates a saved context state', async function() {
@@ -373,12 +402,12 @@ describe('GithubPackage', function() {
await githubPackage.scheduleActiveContextUpdate();
- const repository = await contextPool.getContext(workdirPath).getRepositoryPromise();
- assert.isNotNull(repository);
+ assert.isTrue(contextPool.getContext(workdirPath).isPresent());
+ const repository = contextPool.getContext(workdirPath).getRepository();
repository.destroy();
- assert.isNull(githubPackage.getActiveRepository());
+ assert.isTrue(githubPackage.getActiveRepository().isAbsent());
});
// Don't worry about this on Windows as it's not a common op
@@ -411,11 +440,10 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.activate();
- await until('change observers have started', async () => {
- const observers = await Promise.all([workdirPath1, workdirPath2].map(workdir => {
- return contextPool.getContext(workdir).getChangeObserverPromise();
- }));
- return observers.every(observer => observer && observer.started);
+ await until('change observers have started', () => {
+ return [workdirPath1, workdirPath2].every(workdir => {
+ return contextPool.getContext(workdir).getChangeObserver().isStarted();
+ });
});
[atomGitRepository1, atomGitRepository2] = githubPackage.project.getRepositories();
@@ -467,19 +495,18 @@ describe('GithubPackage', function() {
describe('createRepositoryForProjectPath()', function() {
it('creates and sets a repository for the given project path', async function() {
- const workdirPath1 = await cloneRepository('three-files');
- const workdirPath2 = await cloneRepository('three-files');
- const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
- fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
- project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
+ const nonRepositoryPath = await getTempDir();
+ project.setPaths([nonRepositoryPath]);
+
await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
+ assert.isTrue(githubPackage.getActiveRepository().isEmpty());
+ assert.isFalse(githubPackage.getActiveRepository().isAbsent());
- await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
- await assert.async.isNull(githubPackage.getActiveRepository());
await githubPackage.createRepositoryForProjectPath(nonRepositoryPath);
- await contextPool.getContext(nonRepositoryPath).getRepositoryPromise();
- assert.isOk(githubPackage.getActiveRepository());
+ assert.isTrue(githubPackage.getActiveRepository().isPresent());
assert.strictEqual(
githubPackage.getActiveRepository(),
await contextPool.getContext(nonRepositoryPath).getRepository(),
@@ -489,7 +516,7 @@ describe('GithubPackage', function() {
describe('serialized state', function() {
function resolutionProgressFrom(pkg, workdir) {
- return pkg.getContextPool().getContext(workdir).getResolutionProgressPromise();
+ return pkg.getContextPool().getContext(workdir).getResolutionProgress();
}
it('restores nonempty resolution progress', async function() {
@@ -501,7 +528,8 @@ describe('GithubPackage', function() {
await githubPackage.scheduleActiveContextUpdate();
// Record some state to recover later.
- const resolutionMergeConflict0 = await resolutionProgressFrom(githubPackage, workdirMergeConflict);
+ const resolutionMergeConflict0 = resolutionProgressFrom(githubPackage, workdirMergeConflict);
+ await assert.async.isTrue(resolutionMergeConflict0.loaded);
resolutionMergeConflict0.reportMarkerCount('modified-on-both-ours.txt', 3);
const payload = githubPackage.serialize();
@@ -517,10 +545,10 @@ describe('GithubPackage', function() {
await githubPackage1.activate(payload);
await githubPackage1.scheduleActiveContextUpdate();
- const resolutionMergeConflict1 = await resolutionProgressFrom(githubPackage1, workdirMergeConflict);
- const resolutionNoConflict1 = await resolutionProgressFrom(githubPackage1, workdirNoConflict);
+ const resolutionMergeConflict1 = resolutionProgressFrom(githubPackage1, workdirMergeConflict);
+ const resolutionNoConflict1 = resolutionProgressFrom(githubPackage1, workdirNoConflict);
- assert.isFalse(resolutionMergeConflict1.isEmpty());
+ await assert.async.isFalse(resolutionMergeConflict1.isEmpty());
assert.equal(resolutionMergeConflict1.getRemaining('modified-on-both-ours.txt'), 3);
assert.isTrue(resolutionNoConflict1.isEmpty());
From b6dc78164faf4dda8dc58767cdc039c4e6295e4c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 14:55:59 -0400
Subject: [PATCH 101/161] Wait for context to finish updating
---
test/github-package.test.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 097371aa1a..febdabad51 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -252,13 +252,17 @@ describe('GithubPackage', function() {
cloneRepository('three-files'),
]);
project.setPaths([workdirPath1, workdirPath2]);
+
await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
const repository2 = contextPool.getContext(workdirPath2).getRepository();
assert.isTrue(githubPackage.getActiveRepository().isAbsent());
+ const updatePromise = githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
workspace.open(path.join(workdirPath2, 'b.txt'));
- await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+ await updatePromise;
assert.strictEqual(githubPackage.getActiveRepository(), repository2);
});
From a1bffc331099643f21ff6155eb5726b8d45bd9e1 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 15:29:21 -0400
Subject: [PATCH 102/161] Boolean logic is hard, okay
---
lib/models/repository.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 1ad962de08..1058d1a8cf 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -71,7 +71,7 @@ export default class Repository {
}
getLoadPromise() {
- return this.isAbsent() ? this.loadPromise : Promise.reject(new Error('An absent repository will never load'));
+ return this.isAbsent() ? Promise.reject(new Error('An absent repository will never load')) : this.loadPromise;
}
/*
From 54d0da7e251d8eb0bb34492d879762f251921344 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Mon, 17 Apr 2017 15:29:42 -0400
Subject: [PATCH 103/161] Await context updates correctly
---
test/github-package.test.js | 29 +++++++++++++++++++----------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/test/github-package.test.js b/test/github-package.test.js
index febdabad51..bec0ddbe82 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -33,6 +33,12 @@ describe('GithubPackage', function() {
atomEnv.destroy();
});
+ function contextUpdateAfter(chunk) {
+ const updatePromise = githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+ chunk();
+ return updatePromise;
+ }
+
describe('activate()', function() {
it('uses models from preexisting projects', async function() {
const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([
@@ -279,8 +285,7 @@ describe('GithubPackage', function() {
assert.isFalse(contextPool.getContext(workdirPath2).isPresent());
- workspace.open(path.join(workdirPath2, 'c.txt'));
- await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+ await contextUpdateAfter(() => workspace.open(path.join(workdirPath2, 'c.txt')));
assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
@@ -293,12 +298,12 @@ describe('GithubPackage', function() {
]);
project.setPaths([workdirPath1]);
await githubPackage.activate();
-
- workspace.open(path.join(workdirPath2, 'c.txt'));
await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- workspace.getActivePane().destroyActiveItem();
+ await contextUpdateAfter(() => workspace.open(path.join(workdirPath2, 'c.txt')));
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
+ await contextUpdateAfter(() => workspace.getActivePane().destroyActiveItem());
assert.isFalse(contextPool.getContext(workdirPath2).isPresent());
});
});
@@ -325,12 +330,13 @@ describe('GithubPackage', function() {
assert.equal(githubPackage.getActiveWorkdir(), workdirPath2);
});
- it('uses an empty context when the active item is not in a git repository', async function() {
+ it('uses an absent context when the active item is not in a git repository', async function() {
const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
const workdir = await cloneRepository('three-files');
project.setPaths([nonRepositoryPath, workdir]);
await writeFile(path.join(nonRepositoryPath, 'a.txt'), 'stuff');
- await workspace.open(path.join(nonRepositoryPath, 'a.txt'));
+
+ workspace.open(path.join(nonRepositoryPath, 'a.txt'));
await githubPackage.scheduleActiveContextUpdate();
@@ -351,14 +357,16 @@ describe('GithubPackage', function() {
assert.equal(githubPackage.getActiveWorkdir(), workdirPath1);
});
- it('uses a null context with a single open project without a git workdir', async function() {
- const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
+ it('uses an empty context with a single open project without a git workdir', async function() {
+ const nonRepositoryPath = await getTempDir();
project.setPaths([nonRepositoryPath]);
await githubPackage.scheduleActiveContextUpdate();
+ await githubPackage.getActiveRepository().getLoadPromise();
- assert.isFalse(contextPool.getContext(nonRepositoryPath).isPresent());
+ assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent());
assert.isTrue(githubPackage.getActiveRepository().isEmpty());
+ assert.isFalse(githubPackage.getActiveRepository().isAbsent());
});
it('activates a saved context state', async function() {
@@ -504,6 +512,7 @@ describe('GithubPackage', function() {
await githubPackage.activate();
await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+ await githubPackage.getActiveRepository().getLoadPromise();
assert.isTrue(githubPackage.getActiveRepository().isEmpty());
assert.isFalse(githubPackage.getActiveRepository().isAbsent());
From 730b5b3d0b364ed10592af498c6c35165024f1b1 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 11:08:43 -0400
Subject: [PATCH 104/161] getTempDir() generates the parent directory itself
---
lib/models/discard-history.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/models/discard-history.js b/lib/models/discard-history.js
index 7c71687522..53ca3c2f6b 100644
--- a/lib/models/discard-history.js
+++ b/lib/models/discard-history.js
@@ -118,7 +118,7 @@ export default class DiscardHistory {
}
async expandBlobsToFilesInTempFolder(snapshots) {
- const tempFolderPath = await getTempDir(path.join(os.tmpdir(), 'github-discard-history-'));
+ const tempFolderPath = await getTempDir('github-discard-history-');
const pathPromises = snapshots.map(async ({filePath, beforeSha, afterSha}) => {
const dir = path.dirname(path.join(tempFolderPath, filePath));
await mkdirp(dir);
From 72d5f1716417d5cce28b4cc9654e68d6c03a08a9 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 11:09:02 -0400
Subject: [PATCH 105/161] DiscardHistory requires a working directory path, not
a function
---
lib/models/repository-states/present.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index be44c1b0a7..be6fe97a5e 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -72,7 +72,7 @@ export default class Present extends State {
this.createBlob.bind(this),
this.expandBlobToFile.bind(this),
this.mergeFile.bind(this),
- this.workdir.bind(this),
+ this.workdir(),
{maxHistoryLength: 60},
);
}
From ed6955690f69007a931d65b072033e1abe84b2d7 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 11:09:26 -0400
Subject: [PATCH 106/161] Avoid PropTypes warnings by providing null models
---
test/controllers/root-controller.test.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js
index c12a3ef049..dfa44c1cb1 100644
--- a/test/controllers/root-controller.test.js
+++ b/test/controllers/root-controller.test.js
@@ -8,6 +8,8 @@ import dedent from 'dedent-js';
import {cloneRepository, buildRepository} from '../helpers';
import {writeFile} from '../../lib/helpers';
import {GitError} from '../../lib/git-shell-out-strategy';
+import Repository from '../../lib/models/repository';
+import ResolutionProgress from '../../lib/models/conflicts/resolution-progress';
import RootController from '../../lib/controllers/root-controller';
@@ -25,6 +27,9 @@ describe('RootController', function() {
workspaceElement = atomEnv.views.getView(workspace);
+ const absentRepository = Repository.absent();
+ const emptyResolutionProgress = new ResolutionProgress();
+
confirm = sinon.stub(atomEnv, 'confirm');
app = (
);
});
@@ -636,6 +643,7 @@ describe('RootController', function() {
const contents2 = fs.readFileSync(absFilePath, 'utf8');
assert.notEqual(contents1, contents2);
await repository.refresh();
+
unstagedFilePatch = await repository.getFilePatchForPath('sample.js');
wrapper.setState({filePatch: unstagedFilePatch});
await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(2, 4)));
From 76d9c1919afd6e260f4be8bcb02fd179cf3764fb Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 11:09:43 -0400
Subject: [PATCH 107/161] Wait for checkout operations to complete
---
test/controllers/status-bar-tile-controller.test.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js
index 8feedd44ff..4a82f768b4 100644
--- a/test/controllers/status-bar-tile-controller.test.js
+++ b/test/controllers/status-bar-tile-controller.test.js
@@ -103,6 +103,8 @@ describe('StatusBarTileController', function() {
assert.equal(tip.querySelector('select').value, 'master');
selectOption(tip, 'branch');
+ await assert.async.isFalse(wrapper.instance().getWrappedComponentInstance().state.inProgress);
+
const branch1 = await repository.getCurrentBranch();
assert.equal(branch1.getName(), 'branch');
assert.isFalse(branch1.isDetached());
@@ -111,6 +113,8 @@ describe('StatusBarTileController', function() {
assert.equal(tip.querySelector('select').value, 'branch');
selectOption(tip, 'master');
+ await assert.async.isFalse(wrapper.instance().getWrappedComponentInstance().state.inProgress);
+
const branch2 = await repository.getCurrentBranch();
assert.equal(branch2.getName(), 'master');
assert.isFalse(branch2.isDetached());
From 8268a7190d9cc3fbabea1f97399496ec2b41a8ab Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 11:25:52 -0400
Subject: [PATCH 108/161] Because I changed the behavior of getTempDir for some
reason
---
lib/git-prompt-server.js | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/lib/git-prompt-server.js b/lib/git-prompt-server.js
index dcda296843..8c62ad3fa1 100644
--- a/lib/git-prompt-server.js
+++ b/lib/git-prompt-server.js
@@ -1,6 +1,5 @@
import net from 'net';
import path from 'path';
-import os from 'os';
import {Emitter} from 'event-kit';
@@ -24,8 +23,7 @@ export default class GitPromptServer {
// TODO: [mkt] Windows?? yes.
this.promptForInput = promptForInput;
const windows = process.platform === 'win32';
- const prefix = windows ? os.tmpdir() : '/tmp';
- this.tmpFolderPath = await getTempDir(path.join(prefix, 'github-'));
+ this.tmpFolderPath = await getTempDir('github-');
const credentialHelper = {};
const askPass = {};
From b23c87733a92e46a51b7aa231491515b54fba175 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:27:04 -0400
Subject: [PATCH 109/161] Don't conceal errors
---
lib/controllers/git-tab-controller.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js
index 96e30e5a28..94beda44fe 100644
--- a/lib/controllers/git-tab-controller.js
+++ b/lib/controllers/git-tab-controller.js
@@ -275,6 +275,8 @@ export default class GitTabController {
`Cannot abort because ${e.path} is both dirty and staged.`,
{dismissable: true},
);
+ } else {
+ throw e;
}
}
return etch.update(this);
From 0ea1b57f7ba8f893fa10b1b3f2203388358690a5 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:27:19 -0400
Subject: [PATCH 110/161] Always return a Promise from stageFiles and
unstageFiles
---
lib/git-shell-out-strategy.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js
index 7324fcfa8e..fb847ea135 100644
--- a/lib/git-shell-out-strategy.js
+++ b/lib/git-shell-out-strategy.js
@@ -236,13 +236,13 @@ export default class GitShellOutStrategy {
* Staging/Unstaging files and patches and committing
*/
stageFiles(paths) {
- if (paths.length === 0) { return null; }
+ if (paths.length === 0) { return Promise.resolve(null); }
const args = ['add'].concat(paths);
return this.exec(args, {writeOperation: true});
}
unstageFiles(paths, commit = 'HEAD') {
- if (paths.length === 0) { return null; }
+ if (paths.length === 0) { return Promise.resolve(null); }
const args = ['reset', commit, '--'].concat(paths);
return this.exec(args, {writeOperation: true});
}
From 1b8e85f0112577f670a17cd17567b4b9ca87097d Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:27:33 -0400
Subject: [PATCH 111/161] Use .refresh() for invalidation to trigger didUpdate
---
lib/models/repository-states/present.js | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index be6fe97a5e..ade6925359 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -45,15 +45,13 @@ function invalidate() {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
- const cache = this.cache;
-
return original.apply(this, args).then(
result => {
- cache.clear();
+ this.refresh();
return result;
},
err => {
- cache.clear();
+ this.refresh();
return Promise.reject(err);
},
);
From bc70ab9c3e4f8d7da1d18503ae36621930f69e3a Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:27:46 -0400
Subject: [PATCH 112/161] Await props.commit too
---
lib/views/commit-view.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js
index 6a5c85dc1b..dd91bab111 100644
--- a/lib/views/commit-view.js
+++ b/lib/views/commit-view.js
@@ -106,7 +106,7 @@ export default class CommitView {
@autobind
async commit() {
if (await this.props.prepareToCommit() && this.isCommitButtonEnabled()) {
- this.props.commit(this.editor.getText());
+ await this.props.commit(this.editor.getText());
} else {
this.focus();
}
From 8c5c5fedf4c5153632fe1a3b626412b4120e05a2 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:28:09 -0400
Subject: [PATCH 113/161] :fire: that doesn't need to be a thing any more
---
test/models/workdir-context-pool.test.js | 28 ------------------------
1 file changed, 28 deletions(-)
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index 25ca8a4552..783feef386 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -42,15 +42,6 @@ describe('WorkdirContextPool', function() {
assert.isTrue(pool.getContext(workingDirectory).isPresent());
});
- it('optionally provides a preinitialized repository', function() {
- const existingRepo = new Repository(workingDirectory);
-
- pool.add(workingDirectory, {repository: existingRepo});
-
- const addedRepo = pool.getContext(workingDirectory).getRepository();
- assert.strictEqual(addedRepo, existingRepo);
- });
-
it('is a no-op if the working directory already has a context', function() {
pool.add(workingDirectory);
assert.equal(pool.size(), 1);
@@ -119,25 +110,6 @@ describe('WorkdirContextPool', function() {
assert.notStrictEqual(original, replaced);
});
-
- it('passes a Repository to the created WorkdirContext', async function() {
- const directory = await cloneRepository('three-files');
-
- pool.add(directory);
- assert.equal(pool.size(), 1);
- assert.isTrue(pool.getContext(directory).isPresent());
- const original = pool.getContext(directory).getRepository();
-
- const replacement = await buildRepository(directory);
-
- pool.replace(directory, {repository: replacement});
- assert.equal(pool.size(), 1);
- assert.isTrue(pool.getContext(directory).isPresent());
- const replaced = pool.getContext(directory).getRepository();
-
- assert.notStrictEqual(original, replaced);
- assert.strictEqual(replacement, replaced);
- });
});
describe('remove', function() {
From 6570cdb042aa91141f1599de93cb3242d8b8ff21 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:34:28 -0400
Subject: [PATCH 114/161] Prefer "await asser.async" to the "next render"
promises
---
test/controllers/git-tab-controller.test.js | 116 +++++++++-----------
1 file changed, 50 insertions(+), 66 deletions(-)
diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js
index cfaa611b75..e9ed77bd59 100644
--- a/test/controllers/git-tab-controller.test.js
+++ b/test/controllers/git-tab-controller.test.js
@@ -8,7 +8,7 @@ import until from 'test-until';
import GitTabController from '../../lib/controllers/git-tab-controller';
import {cloneRepository, buildRepository} from '../helpers';
-import {AbortMergeError, CommitError} from '../../lib/models/repository';
+import Repository, {AbortMergeError, CommitError} from '../../lib/models/repository';
import ResolutionProgress from '../../lib/models/conflicts/resolution-progress';
describe('GitTabController', function() {
@@ -59,16 +59,17 @@ describe('GitTabController', function() {
const repository1 = await buildRepository(workdirPath1);
const workdirPath2 = await cloneRepository('three-files');
const repository2 = await buildRepository(workdirPath2);
+
fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'a change\n');
fs.unlinkSync(path.join(workdirPath1, 'b.txt'));
const controller = new GitTabController({
workspace, commandRegistry, resolutionProgress, refreshResolutionProgress,
- repository: null,
+ repository: Repository.absent(),
});
// Renders empty GitTabView when there is no active repository
assert.isDefined(controller.refs.gitPanel);
- assert.isNull(controller.getActiveRepository());
+ assert.isTrue(controller.getActiveRepository().isAbsent());
assert.isDefined(controller.refs.gitPanel.refs.noRepoMessage);
// Fetches data when a new repository is assigned
@@ -156,9 +157,7 @@ describe('GitTabController', function() {
const workdirPath = await cloneRepository('merge-conflict');
const repository = await buildRepository(workdirPath);
- await repository.git.merge('origin/branch')
- .then(() => { throw new Error('Expected merge to throw an error'); })
- .catch(() => true);
+ await assert.isRejected(repository.git.merge('origin/branch'));
const controller = new GitTabController({
workspace, commandRegistry, repository,
@@ -433,28 +432,26 @@ describe('GitTabController', function() {
const stagingView = controller.refs.gitPanel.refs.stagingView;
const commitView = controller.refs.gitPanel.refs.commitViewController.refs.commitView;
- assert.equal(stagingView.props.unstagedChanges.length, 2);
- assert.equal(stagingView.props.stagedChanges.length, 0);
- await stagingView.dblclickOnItem({}, stagingView.props.unstagedChanges[0]).stageOperationPromise;
- repository.refresh();
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
- await stagingView.dblclickOnItem({}, stagingView.props.unstagedChanges[0]).stageOperationPromise;
- repository.refresh();
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
- assert.equal(stagingView.props.unstagedChanges.length, 0);
- assert.equal(stagingView.props.stagedChanges.length, 2);
- await stagingView.dblclickOnItem({}, stagingView.props.stagedChanges[1]).stageOperationPromise;
- repository.refresh();
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
- assert.equal(stagingView.props.unstagedChanges.length, 1);
- assert.equal(stagingView.props.stagedChanges.length, 1);
+ assert.lengthOf(stagingView.props.unstagedChanges, 2);
+ assert.lengthOf(stagingView.props.stagedChanges, 0);
+
+ stagingView.dblclickOnItem({}, stagingView.props.unstagedChanges[0]);
+
+ await assert.async.lengthOf(stagingView.props.unstagedChanges, 1);
+ assert.lengthOf(stagingView.props.stagedChanges, 1);
+
+ stagingView.dblclickOnItem({}, stagingView.props.unstagedChanges[0]);
+
+ await assert.async.lengthOf(stagingView.props.unstagedChanges, 0);
+ assert.lengthOf(stagingView.props.stagedChanges, 2);
+
+ stagingView.dblclickOnItem({}, stagingView.props.stagedChanges[1]);
+
+ await assert.async.lengthOf(stagingView.props.unstagedChanges, 1);
+ assert.lengthOf(stagingView.props.stagedChanges, 1);
commitView.refs.editor.setText('Make it so');
await commitView.commit();
- await controller.getLastModelDataRefreshPromise();
assert.equal((await repository.getLastCommit()).getMessage(), 'Make it so');
});
@@ -463,65 +460,52 @@ describe('GitTabController', function() {
const workdirPath = await cloneRepository('merge-conflict');
const repository = await buildRepository(workdirPath);
- try {
- await repository.git.merge('origin/branch');
- } catch (e) {
- // expected
- }
+ await assert.isRejected(repository.git.merge('origin/branch'));
const controller = new GitTabController({
workspace, commandRegistry, repository,
resolutionProgress, refreshResolutionProgress,
});
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
-
+ await assert.async.isDefined(controller.refs.gitPanel.refs.stagingView);
const stagingView = controller.refs.gitPanel.refs.stagingView;
+
assert.equal(stagingView.props.mergeConflicts.length, 5);
assert.equal(stagingView.props.stagedChanges.length, 0);
const conflict1 = stagingView.props.mergeConflicts.filter(c => c.filePath === 'modified-on-both-ours.txt')[0];
const contentsWithMarkers = fs.readFileSync(path.join(workdirPath, conflict1.filePath), 'utf8');
- assert(contentsWithMarkers.includes('>>>>>>>'));
- assert(contentsWithMarkers.includes('<<<<<<<'));
+ assert.include(contentsWithMarkers, '>>>>>>>');
+ assert.include(contentsWithMarkers, '<<<<<<<');
let choice;
- sinon.stub(atom, 'confirm', () => {
- return choice;
- });
+ sinon.stub(atom, 'confirm', () => choice);
// click Cancel
choice = 1;
- await stagingView.dblclickOnItem({}, conflict1).stageOperationPromise;
- repository.refresh();
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
- assert.equal(atom.confirm.calledOnce, true);
- assert.equal(stagingView.props.mergeConflicts.length, 5);
- assert.equal(stagingView.props.stagedChanges.length, 0);
+ await stagingView.dblclickOnItem({}, conflict1).selectionUpdatePromise;
+
+ await assert.async.lengthOf(stagingView.props.mergeConflicts, 5);
+ assert.lengthOf(stagingView.props.stagedChanges, 0);
+ assert.isTrue(atom.confirm.calledOnce);
// click Stage
choice = 0;
atom.confirm.reset();
- await stagingView.dblclickOnItem({}, conflict1).stageOperationPromise;
- repository.refresh();
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
- assert.equal(atom.confirm.calledOnce, true);
- assert.equal(stagingView.props.mergeConflicts.length, 4);
- assert.equal(stagingView.props.stagedChanges.length, 1);
+ await stagingView.dblclickOnItem({}, conflict1).selectionUpdatePromise;
+
+ await assert.async.lengthOf(stagingView.props.mergeConflicts, 4);
+ assert.lengthOf(stagingView.props.stagedChanges, 1);
+ assert.isTrue(atom.confirm.calledOnce);
// clear merge markers
const conflict2 = stagingView.props.mergeConflicts.filter(c => c.filePath === 'modified-on-both-theirs.txt')[0];
atom.confirm.reset();
fs.writeFileSync(path.join(workdirPath, conflict2.filePath), 'text with no merge markers');
- await stagingView.dblclickOnItem({}, conflict2).stageOperationPromise;
- repository.refresh();
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
- assert.equal(atom.confirm.called, false);
- assert.equal(stagingView.props.mergeConflicts.length, 3);
- assert.equal(stagingView.props.stagedChanges.length, 2);
+ stagingView.dblclickOnItem({}, conflict2);
+
+ await assert.async.lengthOf(stagingView.props.mergeConflicts, 3);
+ assert.lengthOf(stagingView.props.stagedChanges, 2);
+ assert.isFalse(atom.confirm.called);
});
it('avoids conflicts with pending file staging operations', async function() {
@@ -532,11 +516,11 @@ describe('GitTabController', function() {
const controller = new GitTabController({
workspace, commandRegistry, repository, resolutionProgress, refreshResolutionProgress,
});
- await controller.getLastModelDataRefreshPromise();
- await etch.getScheduler().getNextUpdatePromise();
+
+ await assert.async.isDefined(controller.refs.gitPanel.refs.stagingView);
const stagingView = controller.refs.gitPanel.refs.stagingView;
- assert.equal(stagingView.props.unstagedChanges.length, 2);
+ assert.lengthOf(stagingView.props.unstagedChanges, 2);
// ensure staging the same file twice does not cause issues
// second stage action is a no-op since the first staging operation is in flight
@@ -544,15 +528,15 @@ describe('GitTabController', function() {
stagingView.confirmSelectedItems();
await file1StagingPromises.stageOperationPromise;
- repository.refresh();
await file1StagingPromises.selectionUpdatePromise;
- assert.equal(stagingView.props.unstagedChanges.length, 1);
+
+ await assert.async.lengthOf(stagingView.props.unstagedChanges, 1);
const file2StagingPromises = stagingView.confirmSelectedItems();
await file2StagingPromises.stageOperationPromise;
- repository.refresh();
await file2StagingPromises.selectionUpdatePromise;
- assert.equal(stagingView.props.unstagedChanges.length, 0);
+
+ await assert.async.lengthOf(stagingView.props.unstagedChanges, 0);
});
it('updates file status and paths when changed', async function() {
From 62cb71b41fd032d524f06898d3f189bd7b3d70cc Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:44:45 -0400
Subject: [PATCH 115/161] ResolutionProgress constructor no longer accepts
arguments
---
test/views/staging-view.test.js | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js
index 753109f106..ab7269a5b8 100644
--- a/test/views/staging-view.test.js
+++ b/test/views/staging-view.test.js
@@ -101,7 +101,8 @@ describe('StagingView', function() {
status: {file: 'modified', ours: 'deleted', theirs: 'modified'},
}];
- const resolutionProgress = new ResolutionProgress('abcd1234', {});
+ const resolutionProgress = new ResolutionProgress();
+ resolutionProgress.load('abcd1234', {});
const view = new StagingView({
workingDirectoryPath,
@@ -125,7 +126,8 @@ describe('StagingView', function() {
status: {file: 'modified', ours: 'deleted', theirs: 'modified'},
}];
- const resolutionProgress = new ResolutionProgress('abcd1234', {
+ const resolutionProgress = new ResolutionProgress();
+ resolutionProgress.load('abcd1234', {
revision: 'abcd1234',
paths: {
[path.join(workingDirectoryPath, 'conflicted-path')]: 10,
@@ -154,7 +156,8 @@ describe('StagingView', function() {
status: {file: 'modified', ours: 'deleted', theirs: 'modified'},
}];
- const resolutionProgress = new ResolutionProgress('abcd1234', {
+ const resolutionProgress = new ResolutionProgress();
+ resolutionProgress.load('abcd1234', {
revision: 'abcd1234',
paths: {
[path.join(workingDirectoryPath, 'conflicted-path')]: 0,
@@ -186,7 +189,8 @@ describe('StagingView', function() {
},
];
- const resolutionProgress = new ResolutionProgress('abcd1234', {
+ const resolutionProgress = new ResolutionProgress();
+ resolutionProgress.load('abcd1234', {
revision: 'abcd1234',
paths: {
[path.join(workingDirectoryPath, 'conflicted-path-0.txt')]: 2,
@@ -218,7 +222,8 @@ describe('StagingView', function() {
status: {file: 'modified', ours: 'deleted', theirs: 'modified'},
}];
- const resolutionProgress = new ResolutionProgress('abcd1234', {
+ const resolutionProgress = new ResolutionProgress();
+ resolutionProgress.load('abcd1234', {
revision: 'abcd1234',
paths: {
[path.join(workingDirectoryPath, 'conflicted-path')]: 0,
From d270367ae5d896a1053bbe53525616244dd086e7 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 14:48:11 -0400
Subject: [PATCH 116/161] PropTypes are our friend
---
.../controllers/file-patch-controller.test.js | 22 +++++--------------
1 file changed, 6 insertions(+), 16 deletions(-)
diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js
index 96012fcbef..3641e36546 100644
--- a/test/controllers/file-patch-controller.test.js
+++ b/test/controllers/file-patch-controller.test.js
@@ -9,6 +9,8 @@ import FilePatch from '../../lib/models/file-patch';
import FilePatchController from '../../lib/controllers/file-patch-controller';
import Hunk from '../../lib/models/hunk';
import HunkLine from '../../lib/models/hunk-line';
+import Repository from '../../lib/models/repository';
+import ResolutionProgress from '../../lib/models/conflicts/resolution-progress';
import Switchboard from '../../lib/switchboard';
describe('FilePatchController', function() {
@@ -30,10 +32,14 @@ describe('FilePatchController', function() {
openFiles = sinon.spy();
const filePatch = new FilePatch('a.txt', 'a.txt', 'modified', [new Hunk(1, 1, 1, 3, '', [])]);
+ const repository = Repository.absent();
+ const resolutionProgress = new ResolutionProgress();
component = (
Date: Tue, 18 Apr 2017 15:01:50 -0400
Subject: [PATCH 117/161] destroy() takes more time on AppVeyor
---
test/models/workdir-context.test.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 07748c599d..99a37114d4 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -110,7 +110,7 @@ describe('WorkdirContext', function() {
await observer.start();
await context.destroy();
- assert.isFalse(observer.isStarted());
+ await assert.async.isFalse(observer.isStarted());
});
it('can be destroyed twice', async function() {
From 8b26484d7af7e2c05176d287a35f7e41979d643e Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 15:06:13 -0400
Subject: [PATCH 118/161] GitPanel :point_right: GitTab for consistency
---
keymaps/git.cson | 4 +-
lib/controllers/git-tab-controller.js | 14 ++--
lib/controllers/root-controller.js | 64 ++++++++---------
lib/controllers/status-bar-tile-controller.js | 6 +-
lib/views/staging-view.js | 2 +-
menus/git.cson | 4 +-
test/controllers/git-tab-controller.test.js | 72 +++++++++----------
test/controllers/root-controller.test.js | 46 ++++++------
.../status-bar-tile-controller.test.js | 10 +--
9 files changed, 111 insertions(+), 111 deletions(-)
diff --git a/keymaps/git.cson b/keymaps/git.cson
index 81abdfa803..98e30a2427 100644
--- a/keymaps/git.cson
+++ b/keymaps/git.cson
@@ -1,6 +1,6 @@
'.workspace':
- 'ctrl-9': 'github:toggle-git-panel'
- 'ctrl-(': 'github:toggle-git-panel-focus' # ctrl-shift-9
+ 'ctrl-9': 'github:toggle-git-tab'
+ 'ctrl-(': 'github:toggle-git-tab-focus' # ctrl-shift-9
'.github-Panel':
'cmd-enter': 'github:commit'
diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js
index 94beda44fe..53402d81ab 100644
--- a/lib/controllers/git-tab-controller.js
+++ b/lib/controllers/git-tab-controller.js
@@ -36,7 +36,7 @@ export default class GitTabController {
const hasUndoHistory = this.props.repository ? this.hasUndoHistory() : false;
return (
);
@@ -155,14 +155,14 @@ export default class RootController extends React.Component {
renderPanels() {
if (!this.props.useLegacyPanels) {
- const gitPanel = this.state.gitPanelActive && (
+ const gitTab = this.state.gitTabActive && (
subtree.getWrappedComponent()}
- onDidCloseItem={() => this.setState({gitPanelActive: false})}
+ onDidCloseItem={() => this.setState({gitTabActive: false})}
stubItemSelector="git-tab-controller">
{ this.gitPanelController = c; }}
+ ref={c => { this.gitTabController = c; }}
className="github-PanelEtchWrapper"
reattachDomNode={false}>
);
- return {gitPanel}{githubPanel}
;
+ return {gitTab}{githubPanel}
;
}
return (
this.setState({gitPanelActive: false})}
- visible={!!this.state.gitPanelActive}>
+ onDidClosePanel={() => this.setState({gitTabActive: false})}
+ visible={!!this.state.gitTabActive}>
{ this.gitPanelController = c; }}
+ ref={c => { this.gitTabController = c; }}
className="github-PanelEtchWrapper"
reattachDomNode={false}>
({gitPanelActive: !state.gitPanelActive}));
+ toggleGitTab() {
+ this.setState(state => ({gitTabActive: !state.gitTabActive}));
}
@autobind
@@ -447,16 +447,16 @@ export default class RootController extends React.Component {
}
@autobind
- toggleGitPanelFocus() {
- if (!this.state.gitPanelActive) {
- this.setState({gitPanelActive: true}, () => this.toggleGitPanelFocus());
+ toggleGitTabFocus() {
+ if (!this.state.gitTabActive) {
+ this.setState({gitTabActive: true}, () => this.toggleGitTabFocus());
return;
}
- if (this.gitPanelHasFocus()) {
+ if (this.gitTabHasFocus()) {
this.props.workspace.getActivePane().activate();
} else {
- this.focusGitPanel();
+ this.focusGitTab();
}
}
@@ -466,21 +466,21 @@ export default class RootController extends React.Component {
}
@autobind
- focusGitPanel() {
- this.gitPanelController.getWrappedComponent().focus();
+ focusGitTab() {
+ this.gitTabController.getWrappedComponent().focus();
}
- gitPanelHasFocus() {
- return this.gitPanelController.getWrappedComponent().isFocused();
+ gitTabHasFocus() {
+ return this.gitTabController.getWrappedComponent().isFocused();
}
// Ensure that the Git panel is visible. Returns a Promise that resolves to `true` if the panel was initially
// hidden or `false` if it was already shown.
@autobind
- ensureGitPanel() {
- if (!this.state.gitPanelActive) {
+ ensureGitTab() {
+ if (!this.state.gitTabActive) {
return new Promise((resolve, reject) => {
- this.setState({gitPanelActive: true}, () => resolve(true));
+ this.setState({gitTabActive: true}, () => resolve(true));
});
}
@@ -501,8 +501,8 @@ export default class RootController extends React.Component {
@autobind
quietlySelectItem(filePath, stagingStatus) {
- if (this.gitPanelController) {
- return this.gitPanelController.getWrappedComponent().quietlySelectItem(filePath, stagingStatus);
+ if (this.gitTabController) {
+ return this.gitTabController.getWrappedComponent().quietlySelectItem(filePath, stagingStatus);
} else {
return null;
}
diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js
index 9f6ea9d4d8..9ba9e2e08b 100644
--- a/lib/controllers/status-bar-tile-controller.js
+++ b/lib/controllers/status-bar-tile-controller.js
@@ -56,14 +56,14 @@ export default class StatusBarTileController extends React.Component {
aheadCount: PropTypes.number,
behindCount: PropTypes.number,
changedFilesCount: PropTypes.number,
- toggleGitPanel: PropTypes.func,
+ toggleGitTab: PropTypes.func,
}
static defaultProps = {
currentBranch: nullBranch,
branches: [],
currentRemote: nullRemote,
- toggleGitPanel: () => {},
+ toggleGitTab: () => {},
}
constructor(props, context) {
@@ -142,7 +142,7 @@ export default class StatusBarTileController extends React.Component {
/>
diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js
index 7a6a419983..cc4205f8ef 100644
--- a/lib/views/staging-view.js
+++ b/lib/views/staging-view.js
@@ -82,7 +82,7 @@ export default class StagingView {
this.subscriptions.add(this.props.commandRegistry.add('atom-workspace', {
'github:stage-all': () => this.stageAll(),
'github:unstage-all': () => this.unstageAll(),
- 'github:undo-last-discard-in-git-panel': () => this.props.hasUndoHistory && this.undoLastDiscard(),
+ 'github:undo-last-discard-in-git-tab': () => this.props.hasUndoHistory && this.undoLastDiscard(),
}));
this.subscriptions.add(this.props.commandRegistry.add(this.refs.unstagedChanges, {
'github:discard-changes-in-selected-files': () => this.discardChanges(),
diff --git a/menus/git.cson b/menus/git.cson
index 0c45c8628a..38334340ea 100644
--- a/menus/git.cson
+++ b/menus/git.cson
@@ -4,7 +4,7 @@
'submenu': [
{
'label': 'Toggle Git Panel'
- 'command': 'github:toggle-git-panel'
+ 'command': 'github:toggle-git-tab'
}
]
}
@@ -16,7 +16,7 @@
'submenu': [
{
'label': 'Toggle'
- 'command': 'github:toggle-git-panel'
+ 'command': 'github:toggle-git-tab'
}
]
}
diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js
index e9ed77bd59..18910e8514 100644
--- a/test/controllers/git-tab-controller.test.js
+++ b/test/controllers/git-tab-controller.test.js
@@ -43,15 +43,15 @@ describe('GitTabController', function() {
});
assert.equal(controller.getActiveRepository(), repository);
- assert.isDefined(controller.refs.gitPanel.refs.repoLoadingMessage);
- assert.isUndefined(controller.refs.gitPanel.refs.stagingView);
- assert.isUndefined(controller.refs.gitPanel.refs.commitView);
+ assert.isDefined(controller.refs.gitTab.refs.repoLoadingMessage);
+ assert.isUndefined(controller.refs.gitTab.refs.stagingView);
+ assert.isUndefined(controller.refs.gitTab.refs.commitView);
await controller.getLastModelDataRefreshPromise();
assert.equal(controller.getActiveRepository(), repository);
- await assert.async.isUndefined(controller.refs.gitPanel.refs.repoLoadingMessage);
- await assert.async.isDefined(controller.refs.gitPanel.refs.stagingView);
- await assert.async.isDefined(controller.refs.gitPanel.refs.commitViewController);
+ await assert.async.isUndefined(controller.refs.gitTab.refs.repoLoadingMessage);
+ await assert.async.isDefined(controller.refs.gitTab.refs.stagingView);
+ await assert.async.isDefined(controller.refs.gitTab.refs.commitViewController);
});
it('keeps the state of the GitTabView in sync with the assigned repository', async function() {
@@ -68,47 +68,47 @@ describe('GitTabController', function() {
});
// Renders empty GitTabView when there is no active repository
- assert.isDefined(controller.refs.gitPanel);
+ assert.isDefined(controller.refs.gitTab);
assert.isTrue(controller.getActiveRepository().isAbsent());
- assert.isDefined(controller.refs.gitPanel.refs.noRepoMessage);
+ assert.isDefined(controller.refs.gitTab.refs.noRepoMessage);
// Fetches data when a new repository is assigned
// Does not update repository instance variable until that data is fetched
await controller.update({repository: repository1});
assert.equal(controller.getActiveRepository(), repository1);
- assert.deepEqual(controller.refs.gitPanel.props.unstagedChanges, await repository1.getUnstagedChanges());
+ assert.deepEqual(controller.refs.gitTab.props.unstagedChanges, await repository1.getUnstagedChanges());
await controller.update({repository: repository2});
assert.equal(controller.getActiveRepository(), repository2);
- assert.deepEqual(controller.refs.gitPanel.props.unstagedChanges, await repository2.getUnstagedChanges());
+ assert.deepEqual(controller.refs.gitTab.props.unstagedChanges, await repository2.getUnstagedChanges());
// Fetches data and updates child view when the repository is mutated
fs.writeFileSync(path.join(workdirPath2, 'a.txt'), 'a change\n');
fs.unlinkSync(path.join(workdirPath2, 'b.txt'));
repository2.refresh();
await controller.getLastModelDataRefreshPromise();
- await assert.async.deepEqual(controller.refs.gitPanel.props.unstagedChanges, await repository2.getUnstagedChanges());
+ await assert.async.deepEqual(controller.refs.gitTab.props.unstagedChanges, await repository2.getUnstagedChanges());
});
it('displays the staged changes since the parent commit when amending', async function() {
const didChangeAmending = sinon.spy();
const workdirPath = await cloneRepository('multiple-commits');
const repository = await buildRepository(workdirPath);
- const ensureGitPanel = () => Promise.resolve(false);
+ const ensureGitTab = () => Promise.resolve(false);
const controller = new GitTabController({
- workspace, commandRegistry, repository, didChangeAmending, ensureGitPanel,
+ workspace, commandRegistry, repository, didChangeAmending, ensureGitTab,
resolutionProgress, refreshResolutionProgress,
isAmending: false,
});
await controller.getLastModelDataRefreshPromise();
- await assert.async.deepEqual(controller.refs.gitPanel.props.stagedChanges, []);
+ await assert.async.deepEqual(controller.refs.gitTab.props.stagedChanges, []);
assert.equal(didChangeAmending.callCount, 0);
await controller.setAmending(true);
assert.equal(didChangeAmending.callCount, 1);
await controller.update({isAmending: true});
assert.deepEqual(
- controller.refs.gitPanel.props.stagedChanges,
+ controller.refs.gitTab.props.stagedChanges,
await controller.getActiveRepository().getStagedChangesSinceParentCommit(),
);
@@ -186,9 +186,9 @@ describe('GitTabController', function() {
const workdirPath = await cloneRepository('three-files');
const repository = await buildRepository(workdirPath);
- const ensureGitPanel = () => Promise.resolve(true);
+ const ensureGitTab = () => Promise.resolve(true);
const controller = new GitTabController({
- workspace, commandRegistry, repository, ensureGitPanel,
+ workspace, commandRegistry, repository, ensureGitTab,
resolutionProgress, refreshResolutionProgress,
});
@@ -199,9 +199,9 @@ describe('GitTabController', function() {
const workdirPath = await cloneRepository('three-files');
const repository = await buildRepository(workdirPath);
- const ensureGitPanel = () => Promise.resolve(false);
+ const ensureGitTab = () => Promise.resolve(false);
const controller = new GitTabController({
- workspace, commandRegistry, repository, ensureGitPanel,
+ workspace, commandRegistry, repository, ensureGitTab,
resolutionProgress, refreshResolutionProgress,
});
@@ -258,8 +258,8 @@ describe('GitTabController', function() {
await controller.getLastModelDataRefreshPromise();
await etch.getScheduler().getNextUpdatePromise();
- const gitPanel = controller.refs.gitPanel;
- const stagingView = gitPanel.refs.stagingView;
+ const gitTab = controller.refs.gitTab;
+ const stagingView = gitTab.refs.stagingView;
sinon.spy(stagingView, 'focus');
@@ -273,12 +273,12 @@ describe('GitTabController', function() {
});
describe('keyboard navigation commands', function() {
- let controller, gitPanel, stagingView, commitView, commitViewController, focusElement;
+ let controller, gitTab, stagingView, commitView, commitViewController, focusElement;
const extractReferences = () => {
- gitPanel = controller.refs.gitPanel;
- stagingView = gitPanel.refs.stagingView;
- commitViewController = gitPanel.refs.commitViewController;
+ gitTab = controller.refs.gitTab;
+ stagingView = gitTab.refs.stagingView;
+ commitViewController = gitTab.refs.commitViewController;
commitView = commitViewController.refs.commitView;
focusElement = stagingView;
@@ -381,10 +381,10 @@ describe('GitTabController', function() {
const didChangeAmending = () => {};
const prepareToCommit = () => Promise.resolve(true);
- const ensureGitPanel = () => Promise.resolve(false);
+ const ensureGitTab = () => Promise.resolve(false);
controller = new GitTabController({
- workspace, commandRegistry, repository, didChangeAmending, prepareToCommit, ensureGitPanel,
+ workspace, commandRegistry, repository, didChangeAmending, prepareToCommit, ensureGitTab,
resolutionProgress, refreshResolutionProgress,
});
await controller.getLastModelDataRefreshPromise();
@@ -422,15 +422,15 @@ describe('GitTabController', function() {
const repository = await buildRepository(workdirPath);
fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n');
fs.unlinkSync(path.join(workdirPath, 'b.txt'));
- const ensureGitPanel = () => Promise.resolve(false);
+ const ensureGitTab = () => Promise.resolve(false);
const controller = new GitTabController({
- workspace, commandRegistry, repository, ensureGitPanel, didChangeAmending: sinon.stub(),
+ workspace, commandRegistry, repository, ensureGitTab, didChangeAmending: sinon.stub(),
resolutionProgress, refreshResolutionProgress,
});
await controller.getLastModelDataRefreshPromise();
await etch.getScheduler().getNextUpdatePromise();
- const stagingView = controller.refs.gitPanel.refs.stagingView;
- const commitView = controller.refs.gitPanel.refs.commitViewController.refs.commitView;
+ const stagingView = controller.refs.gitTab.refs.stagingView;
+ const commitView = controller.refs.gitTab.refs.commitViewController.refs.commitView;
assert.lengthOf(stagingView.props.unstagedChanges, 2);
assert.lengthOf(stagingView.props.stagedChanges, 0);
@@ -466,8 +466,8 @@ describe('GitTabController', function() {
workspace, commandRegistry, repository,
resolutionProgress, refreshResolutionProgress,
});
- await assert.async.isDefined(controller.refs.gitPanel.refs.stagingView);
- const stagingView = controller.refs.gitPanel.refs.stagingView;
+ await assert.async.isDefined(controller.refs.gitTab.refs.stagingView);
+ const stagingView = controller.refs.gitTab.refs.stagingView;
assert.equal(stagingView.props.mergeConflicts.length, 5);
assert.equal(stagingView.props.stagedChanges.length, 0);
@@ -517,8 +517,8 @@ describe('GitTabController', function() {
workspace, commandRegistry, repository, resolutionProgress, refreshResolutionProgress,
});
- await assert.async.isDefined(controller.refs.gitPanel.refs.stagingView);
- const stagingView = controller.refs.gitPanel.refs.stagingView;
+ await assert.async.isDefined(controller.refs.gitTab.refs.stagingView);
+ const stagingView = controller.refs.gitTab.refs.stagingView;
assert.lengthOf(stagingView.props.unstagedChanges, 2);
@@ -549,7 +549,7 @@ describe('GitTabController', function() {
});
await controller.getLastModelDataRefreshPromise();
await etch.getScheduler().getNextUpdatePromise();
- const stagingView = controller.refs.gitPanel.refs.stagingView;
+ const stagingView = controller.refs.gitTab.refs.stagingView;
const [addedFilePatch] = stagingView.props.unstagedChanges;
assert.equal(addedFilePatch.filePath, 'new-file.txt');
diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js
index 22fcd063cf..7c1d87b8d5 100644
--- a/test/controllers/root-controller.test.js
+++ b/test/controllers/root-controller.test.js
@@ -276,7 +276,7 @@ describe('RootController', function() {
});
});
- describe('when the StatusBarTileController calls toggleGitPanel', function() {
+ describe('when the StatusBarTileController calls toggleGitTab', function() {
it('toggles the git panel', async function() {
const workdirPath = await cloneRepository('multiple-commits');
const repository = await buildRepository(workdirPath);
@@ -285,14 +285,14 @@ describe('RootController', function() {
const wrapper = shallow(app);
assert.isFalse(wrapper.find('Panel').prop('visible'));
- wrapper.find('ObserveModelDecorator(StatusBarTileController)').prop('toggleGitPanel')();
+ wrapper.find('ObserveModelDecorator(StatusBarTileController)').prop('toggleGitTab')();
assert.isTrue(wrapper.find('Panel').prop('visible'));
- wrapper.find('ObserveModelDecorator(StatusBarTileController)').prop('toggleGitPanel')();
+ wrapper.find('ObserveModelDecorator(StatusBarTileController)').prop('toggleGitTab')();
assert.isFalse(wrapper.find('Panel').prop('visible'));
});
});
- describe('toggleGitPanel()', function() {
+ describe('toggleGitTab()', function() {
it('toggles the visibility of the Git panel', async function() {
const workdirPath = await cloneRepository('multiple-commits');
const repository = await buildRepository(workdirPath);
@@ -301,14 +301,14 @@ describe('RootController', function() {
const wrapper = shallow(app);
assert.isFalse(wrapper.find('Panel').prop('visible'));
- wrapper.instance().toggleGitPanel();
+ wrapper.instance().toggleGitTab();
assert.isTrue(wrapper.find('Panel').prop('visible'));
- wrapper.instance().toggleGitPanel();
+ wrapper.instance().toggleGitTab();
assert.isFalse(wrapper.find('Panel').prop('visible'));
});
});
- describe('toggleGitPanelFocus()', function() {
+ describe('toggleGitTabFocus()', function() {
let wrapper;
beforeEach(async function() {
@@ -318,44 +318,44 @@ describe('RootController', function() {
app = React.cloneElement(app, {repository});
wrapper = shallow(app);
- sinon.stub(wrapper.instance(), 'focusGitPanel');
+ sinon.stub(wrapper.instance(), 'focusGitTab');
sinon.spy(workspace.getActivePane(), 'activate');
});
it('opens and focuses the Git panel when it is initially closed', function() {
assert.isFalse(wrapper.find('Panel').prop('visible'));
- sinon.stub(wrapper.instance(), 'gitPanelHasFocus').returns(false);
+ sinon.stub(wrapper.instance(), 'gitTabHasFocus').returns(false);
- wrapper.instance().toggleGitPanelFocus();
+ wrapper.instance().toggleGitTabFocus();
assert.isTrue(wrapper.find('Panel').prop('visible'));
- assert.equal(wrapper.instance().focusGitPanel.callCount, 1);
+ assert.equal(wrapper.instance().focusGitTab.callCount, 1);
assert.isFalse(workspace.getActivePane().activate.called);
});
it('focuses the Git panel when it is already open, but blurred', function() {
- wrapper.instance().toggleGitPanel();
- sinon.stub(wrapper.instance(), 'gitPanelHasFocus').returns(false);
+ wrapper.instance().toggleGitTab();
+ sinon.stub(wrapper.instance(), 'gitTabHasFocus').returns(false);
assert.isTrue(wrapper.find('Panel').prop('visible'));
- wrapper.instance().toggleGitPanelFocus();
+ wrapper.instance().toggleGitTabFocus();
assert.isTrue(wrapper.find('Panel').prop('visible'));
- assert.equal(wrapper.instance().focusGitPanel.callCount, 1);
+ assert.equal(wrapper.instance().focusGitTab.callCount, 1);
assert.isFalse(workspace.getActivePane().activate.called);
});
it('blurs the Git panel when it is already open and focused', function() {
- wrapper.instance().toggleGitPanel();
- sinon.stub(wrapper.instance(), 'gitPanelHasFocus').returns(true);
+ wrapper.instance().toggleGitTab();
+ sinon.stub(wrapper.instance(), 'gitTabHasFocus').returns(true);
assert.isTrue(wrapper.find('Panel').prop('visible'));
- wrapper.instance().toggleGitPanelFocus();
+ wrapper.instance().toggleGitTabFocus();
assert.isTrue(wrapper.find('Panel').prop('visible'));
- assert.equal(wrapper.instance().focusGitPanel.callCount, 0);
+ assert.equal(wrapper.instance().focusGitTab.callCount, 0);
assert.isTrue(workspace.getActivePane().activate.called);
});
});
@@ -479,7 +479,7 @@ describe('RootController', function() {
});
});
- describe('ensureGitPanel()', function() {
+ describe('ensureGitTab()', function() {
let wrapper;
beforeEach(async function() {
@@ -492,13 +492,13 @@ describe('RootController', function() {
it('opens the Git panel when it is initially closed', async function() {
assert.isFalse(wrapper.find('Panel').prop('visible'));
- assert.isTrue(await wrapper.instance().ensureGitPanel());
+ assert.isTrue(await wrapper.instance().ensureGitTab());
});
it('does nothing when the Git panel is already open', async function() {
- wrapper.instance().toggleGitPanel();
+ wrapper.instance().toggleGitTab();
assert.isTrue(wrapper.find('Panel').prop('visible'));
- assert.isFalse(await wrapper.instance().ensureGitPanel());
+ assert.isFalse(await wrapper.instance().ensureGitTab());
assert.isTrue(wrapper.find('Panel').prop('visible'));
});
});
diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js
index 4a82f768b4..2895bd9767 100644
--- a/test/controllers/status-bar-tile-controller.test.js
+++ b/test/controllers/status-bar-tile-controller.test.js
@@ -465,9 +465,9 @@ describe('StatusBarTileController', function() {
const workdirPath = await cloneRepository('three-files');
const repository = await buildRepository(workdirPath);
- const toggleGitPanel = sinon.spy();
+ const toggleGitTab = sinon.spy();
- const wrapper = mount(React.cloneElement(component, {repository, toggleGitPanel}));
+ const wrapper = mount(React.cloneElement(component, {repository, toggleGitTab}));
await wrapper.instance().refreshModelData();
assert.equal(wrapper.find('.github-ChangedFilesCount').render().text(), '0 files');
@@ -485,13 +485,13 @@ describe('StatusBarTileController', function() {
const workdirPath = await cloneRepository('three-files');
const repository = await buildRepository(workdirPath);
- const toggleGitPanel = sinon.spy();
+ const toggleGitTab = sinon.spy();
- const wrapper = mount(React.cloneElement(component, {repository, toggleGitPanel}));
+ const wrapper = mount(React.cloneElement(component, {repository, toggleGitTab}));
await wrapper.instance().refreshModelData();
wrapper.find(ChangedFilesCountView).simulate('click');
- assert(toggleGitPanel.calledOnce);
+ assert(toggleGitTab.calledOnce);
});
});
});
From 26b9b5bd0200e5d907ca4acf564e146f0ae67470 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 15:39:05 -0400
Subject: [PATCH 119/161] :fire: unused imports
---
lib/controllers/github-tab-controller.js | 3 +--
lib/views/push-pull-menu-view.js | 1 -
test/models/workdir-context-pool.test.js | 3 +--
3 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/lib/controllers/github-tab-controller.js b/lib/controllers/github-tab-controller.js
index fbb83bbf76..eff2da6f35 100644
--- a/lib/controllers/github-tab-controller.js
+++ b/lib/controllers/github-tab-controller.js
@@ -5,9 +5,8 @@ import yubikiri from 'yubikiri';
import RemotePrController from './remote-pr-controller';
import GithubLoginModel from '../models/github-login-model';
-import {nullBranch} from '../models/branch';
import ObserveModel from '../views/observe-model';
-import {RemotePropType, BranchPropType} from '../prop-types';
+import {RemotePropType} from '../prop-types';
class RemoteSelector extends React.Component {
static propTypes = {
diff --git a/lib/views/push-pull-menu-view.js b/lib/views/push-pull-menu-view.js
index a67fefad0a..79430e887c 100644
--- a/lib/views/push-pull-menu-view.js
+++ b/lib/views/push-pull-menu-view.js
@@ -4,7 +4,6 @@ import {autobind} from 'core-decorators';
import {GitError} from '../git-shell-out-strategy';
import {BranchPropType, RemotePropType} from '../prop-types';
-import {nullBranch} from '../models/branch';
export default class PushPullMenuView extends React.Component {
static propTypes = {
diff --git a/test/models/workdir-context-pool.test.js b/test/models/workdir-context-pool.test.js
index 783feef386..90278b8cda 100644
--- a/test/models/workdir-context-pool.test.js
+++ b/test/models/workdir-context-pool.test.js
@@ -1,8 +1,7 @@
import temp from 'temp';
-import {cloneRepository, buildRepository, sha} from '../helpers';
+import {cloneRepository, sha} from '../helpers';
-import Repository from '../../lib/models/repository';
import WorkdirContextPool from '../../lib/models/workdir-context-pool';
describe('WorkdirContextPool', function() {
From 4583a1819e9b717bf8e3a803afd94d0075577fff Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 16:27:41 -0400
Subject: [PATCH 120/161] Default Repository data to null objects
---
lib/controllers/git-tab-controller.js | 22 +++++++++++++++++--
test/controllers/git-tab-controller.test.js | 24 +++++++++++----------
2 files changed, 33 insertions(+), 13 deletions(-)
diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js
index 53402d81ab..6f043a970a 100644
--- a/lib/controllers/git-tab-controller.js
+++ b/lib/controllers/git-tab-controller.js
@@ -6,6 +6,8 @@ import etch from 'etch';
import GitTabView from '../views/git-tab-view';
import ModelObserver from '../models/model-observer';
+import {nullBranch} from '../models/branch';
+import {nullCommit} from '../models/commit';
import {autobind} from 'core-decorators';
import yubikiri from 'yubikiri';
@@ -18,7 +20,7 @@ export default class GitTabController {
fetchData: this.fetchRepositoryData,
didUpdate: () => {
this.refreshResolutionProgress(false, false);
- return etch.update(this);
+ return this.update();
},
});
this.repositoryObserver.setActiveModel(props.repository);
@@ -32,7 +34,7 @@ export default class GitTabController {
}
render() {
- const modelData = this.repositoryObserver.getActiveModelData() || {fetchInProgress: true};
+ const modelData = this.repositoryObserver.getActiveModelData() || this.defaultRepositoryData();
const hasUndoHistory = this.props.repository ? this.hasUndoHistory() : false;
return (
Date: Tue, 18 Apr 2017 16:27:58 -0400
Subject: [PATCH 121/161] Always emit an update event on state change
---
lib/models/repository.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 1058d1a8cf..b5ca204e55 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -62,6 +62,7 @@ export default class Repository {
this.state = nextState;
this.emitter.emit('did-change-state', {from: currentState, to: this.state});
+ this.emitter.emit('did-update');
return this.state.start();
}
From bd9b66ec6562e0833cc4fdbea8dd99659cda046c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 16:28:57 -0400
Subject: [PATCH 122/161] Hide the Amend checkbox while loading
---
lib/views/commit-view.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js
index dd91bab111..1130c14b08 100644
--- a/lib/views/commit-view.js
+++ b/lib/views/commit-view.js
@@ -50,7 +50,11 @@ export default class CommitView {
}
const showAbortMergeButton = this.props.isMerging || null;
- const showAmendBox = (!this.props.isMerging && !this.props.lastCommit.isUnbornRef()) || null;
+ const showAmendBox = (
+ !this.props.isMerging &&
+ this.props.lastCommit.isPresent() &&
+ !this.props.lastCommit.isUnbornRef()
+ ) || null;
return (
From 863d5208a4f4c4512e4f9f2e8149e5de3c96f679 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 16:31:26 -0400
Subject: [PATCH 123/161] Test the CommitView in the loading state
---
lib/views/commit-view.js | 1 +
test/views/commit-view.test.js | 24 +++++++++++++++++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js
index 1130c14b08..70591951d5 100644
--- a/lib/views/commit-view.js
+++ b/lib/views/commit-view.js
@@ -131,6 +131,7 @@ export default class CommitView {
isCommitButtonEnabled() {
return this.props.stagedChangesExist &&
!this.props.mergeConflictsExist &&
+ this.props.lastCommit.isPresent() &&
this.editor &&
this.editor.getText().length !== 0;
}
diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js
index 3ad0253eac..c5673f3c6c 100644
--- a/test/views/commit-view.test.js
+++ b/test/views/commit-view.test.js
@@ -2,7 +2,7 @@ import {cloneRepository, buildRepository} from '../helpers';
import etch from 'etch';
import until from 'test-until';
-import Commit from '../../lib/models/commit';
+import Commit, {nullCommit} from '../../lib/models/commit';
import CommitView from '../../lib/views/commit-view';
describe('CommitView', function() {
@@ -19,6 +19,28 @@ describe('CommitView', function() {
atomEnv.destroy();
});
+ describe('when the repo is loading', function() {
+ let view;
+
+ beforeEach(function() {
+ view = new CommitView({
+ commandRegistry, lastCommit: nullCommit,
+ stagedChangesExist: false, maximumCharacterLimit: 72, message: '',
+ });
+ });
+
+ it("doesn't show the amend checkbox", function() {
+ assert.isUndefined(view.refs.amend);
+ });
+
+ it('disables the commit button', async function() {
+ view.refs.editor.setText('even with text');
+ await view.update({});
+
+ assert.isTrue(view.refs.commitButton.disabled);
+ });
+ });
+
it('displays the remaining characters limit based on which line is being edited', async function() {
const view = new CommitView({
commandRegistry, lastCommit,
From b25433beb9ead8c91038677b6367c2b37edee551 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 16:31:41 -0400
Subject: [PATCH 124/161] Apply .is-loading to the panel when loading
---
lib/views/git-tab-view.js | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 7155fa0922..b87a1a4848 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -5,6 +5,7 @@ import {Disposable, CompositeDisposable} from 'event-kit';
import etch from 'etch';
import {autobind} from 'core-decorators';
+import cx from 'classnames';
import StagingView from './staging-view';
import GitLogo from './git-logo';
@@ -36,7 +37,7 @@ export default class GitTabView {
render() {
if (this.props.repository.isEmpty()) {
return (
-
+
@@ -48,15 +49,11 @@ export default class GitTabView {
);
- } else if (this.props.fetchInProgress) {
- return (
-
-
Fetching repository data
-
- );
} else {
+ const isLoading = this.props.fetchInProgress || this.props.repository.isLoading();
+
return (
-
+
From 8e8f3dc8a6db46fe71ca271a589ff62008a5cd62 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Tue, 18 Apr 2017 16:31:59 -0400
Subject: [PATCH 125/161] Grey out controls and text while loading
---
styles/git-panel.less | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/styles/git-panel.less b/styles/git-panel.less
index a2e8947b0f..bc77be7457 100644
--- a/styles/git-panel.less
+++ b/styles/git-panel.less
@@ -22,13 +22,12 @@
-webkit-user-select: none;
cursor: default;
- &.is-loading, &.no-repository {
+ &.no-repository {
padding: @component-padding * 3;
text-align: center;
}
&.is-loading {
- font-style: italic;
color: @text-color-subtle;
}
From 94acec7fbb76b271f9404d5535ab3c132176dcaf Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 09:43:52 -0400
Subject: [PATCH 126/161] Undetermined state for package activation delay
---
lib/models/repository-states/index.js | 3 ++-
lib/models/repository-states/undetermined.js | 14 +++++++++++
lib/models/repository.js | 24 +++++++++++++++---
lib/models/workdir-context.js | 26 ++++++++++++--------
test/models/workdir-context.test.js | 11 ++++++++-
5 files changed, 62 insertions(+), 16 deletions(-)
create mode 100644 lib/models/repository-states/undetermined.js
diff --git a/lib/models/repository-states/index.js b/lib/models/repository-states/index.js
index 954b020473..e4ed4ded2c 100644
--- a/lib/models/repository-states/index.js
+++ b/lib/models/repository-states/index.js
@@ -1,6 +1,7 @@
export {expectedDelegates} from './state';
-export {default as InitialState} from './loading';
+export {default as LoadingState} from './loading';
+export {default as UndeterminedState} from './undetermined';
export {default as AbsentState} from './absent';
// Load and register remaining states
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
new file mode 100644
index 0000000000..d06731c10d
--- /dev/null
+++ b/lib/models/repository-states/undetermined.js
@@ -0,0 +1,14 @@
+import State from './state';
+
+export default class Undetermined extends State {
+ start() {
+ setTimeout(() => this.transitionTo('Absent'), 1000);
+ return Promise.resolve();
+ }
+
+ isLoading() {
+ return true;
+ }
+}
+
+State.register(Undetermined);
diff --git a/lib/models/repository.js b/lib/models/repository.js
index b5ca204e55..6da5964d0e 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -5,11 +5,13 @@ import {Emitter} from 'event-kit';
import CompositeGitStrategy from '../composite-git-strategy';
import {readFile} from '../helpers';
import Remote, {nullRemote} from './remote';
-import {InitialState, AbsentState} from './repository-states';
+import {LoadingState, AbsentState, UndeterminedState} from './repository-states';
const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
const ABSENT = {};
+const UNDETERMINED = {};
+const LOADING = {};
export class AbortMergeError extends Error {
constructor(code, filePath) {
@@ -30,8 +32,18 @@ export class CommitError extends Error {
}
}
+function getStateConstructor(token) {
+ if (token === ABSENT) {
+ return AbsentState;
+ } else if (token === UNDETERMINED) {
+ return UndeterminedState;
+ } else {
+ return LoadingState;
+ }
+}
+
export default class Repository {
- constructor(workingDirectoryPath, gitStrategy = null) {
+ constructor(workingDirectoryPath, gitStrategy = null, initialState = LOADING) {
this.workingDirectoryPath = workingDirectoryPath;
this.git = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
@@ -48,11 +60,15 @@ export default class Repository {
});
});
- this.transitionTo(workingDirectoryPath !== ABSENT ? InitialState : AbsentState);
+ this.transitionTo(getStateConstructor(initialState));
}
static absent() {
- return new Repository(ABSENT);
+ return new Repository(null, null, ABSENT);
+ }
+
+ static undetermined() {
+ return new Repository(null, null, UNDETERMINED);
}
// State management //////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 379d95e2e0..c4d1c1a1c9 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -6,7 +6,10 @@ import ResolutionProgress from './conflicts/resolution-progress';
import FileSystemChangeObserver from './file-system-change-observer';
import WorkspaceChangeObserver from './workspace-change-observer';
+const initRepoState = Symbol('initRepoState');
+
const ABSENT = {};
+const UNDETERMINED = {};
/*
* Bundle of model objects associated with a git working directory.
@@ -26,13 +29,12 @@ export default class WorkdirContext {
* - `options.promptCallback`: Callback used to collect information interactively through Atom.
*/
constructor(directory, options = {}) {
- if (directory !== ABSENT) {
- this.directory = directory;
- this.repository = new Repository(directory);
- } else {
- this.directory = null;
- this.repository = Repository.absent();
- }
+ this.directory = directory;
+ this.repository = (token => {
+ if (token === ABSENT) { return Repository.absent(); }
+ if (token === UNDETERMINED) { return Repository.undetermined(); }
+ return new Repository(directory);
+ })(options[initRepoState]);
const {window: theWindow, workspace, promptCallback} = options;
this.savedResolutionProgress = options.savedResolutionProgress || {};
@@ -59,6 +61,10 @@ export default class WorkdirContext {
this.repositoryChangedState({from: null, to: this.repository.state});
}
+ static undetermined() {
+ return new WorkdirContext(null, {[initRepoState]: UNDETERMINED});
+ }
+
/**
* Respond to changes in `Repository` state. Load resolution progress and start the change observer when it becomes
* present. Stop the change observer when it is destroyed. Re-broadcast the event to context subscribers
@@ -178,9 +184,9 @@ export default class WorkdirContext {
}
}
-class NullWorkdirContext extends WorkdirContext {
+class AbsentWorkdirContext extends WorkdirContext {
constructor() {
- super(ABSENT);
+ super(null, {[initRepoState]: ABSENT});
}
isPresent() {
@@ -188,4 +194,4 @@ class NullWorkdirContext extends WorkdirContext {
}
}
-export const nullWorkdirContext = new NullWorkdirContext();
+export const absentWorkdirContext = new AbsentWorkdirContext();
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 99a37114d4..9bc1858150 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -2,7 +2,7 @@ import {CompositeDisposable, Disposable} from 'event-kit';
import {cloneRepository, sha} from '../helpers';
-import WorkdirContext from '../../lib/models/workdir-context';
+import WorkdirContext, {absentWorkdirContext} from '../../lib/models/workdir-context';
describe('WorkdirContext', function() {
let context, workingDirectory, subs;
@@ -122,4 +122,13 @@ describe('WorkdirContext', function() {
await context.destroy();
assert.isTrue(context.isDestroyed());
});
+
+ it('exports a singleton containing a Repository in the absent state', function() {
+ assert.isTrue(absentWorkdirContext.getRepository().isAbsent());
+ });
+
+ it('can be constructed containing a Repository in an undetermined state', function() {
+ const undetermined = WorkdirContext.undetermined();
+ assert.isTrue(undetermined.getRepository().isLoading());
+ });
});
From c22b6d1a4cab2d76abe8433b325623260db120cf Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 09:53:26 -0400
Subject: [PATCH 127/161] Give Undetermined its own state probe method
---
lib/models/repository-states/state.js | 11 ++++++++---
lib/models/repository-states/undetermined.js | 2 +-
lib/models/repository.js | 3 ++-
test/models/workdir-context.test.js | 2 +-
4 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index 9330ae0416..fae51ea18b 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -39,17 +39,22 @@ export default class State {
// Allow external callers to identify which state a Repository is in if necessary.
@shouldDelegate
- isLoading() {
+ isUndetermined() {
return false;
}
@shouldDelegate
- isEmpty() {
+ isAbsent() {
return false;
}
@shouldDelegate
- isAbsent() {
+ isLoading() {
+ return false;
+ }
+
+ @shouldDelegate
+ isEmpty() {
return false;
}
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
index d06731c10d..96ebd38bce 100644
--- a/lib/models/repository-states/undetermined.js
+++ b/lib/models/repository-states/undetermined.js
@@ -6,7 +6,7 @@ export default class Undetermined extends State {
return Promise.resolve();
}
- isLoading() {
+ isUndetermined() {
return true;
}
}
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 6da5964d0e..2daa560db1 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -186,9 +186,10 @@ export default class Repository {
// This list should match the methods decorated with @shouldDelegate in `lib/models/repository-states/state.js`. A test
// case in `test/models/repository.test.js` ensures that these sets match.
const delegates = [
+ 'isUndetermined',
+ 'isAbsent',
'isLoading',
'isEmpty',
- 'isAbsent',
'isPresent',
'isDestroyed',
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 9bc1858150..47edba2829 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -129,6 +129,6 @@ describe('WorkdirContext', function() {
it('can be constructed containing a Repository in an undetermined state', function() {
const undetermined = WorkdirContext.undetermined();
- assert.isTrue(undetermined.getRepository().isLoading());
+ assert.isTrue(undetermined.getRepository().isUndetermined());
});
});
From 6c05e1aae5651f42c0f68c5221f1d24f9e9d50d5 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 09:59:15 -0400
Subject: [PATCH 128/161] Return absentWorkdirContexts from the pool
---
lib/models/workdir-context-pool.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/models/workdir-context-pool.js b/lib/models/workdir-context-pool.js
index c5db10fac9..278e532722 100644
--- a/lib/models/workdir-context-pool.js
+++ b/lib/models/workdir-context-pool.js
@@ -1,7 +1,7 @@
import compareSets from 'compare-sets';
import {Emitter, CompositeDisposable} from 'event-kit';
-import WorkdirContext, {nullWorkdirContext} from './workdir-context';
+import WorkdirContext, {absentWorkdirContext} from './workdir-context';
/**
* Manage a WorkdirContext for each open directory.
@@ -26,7 +26,7 @@ export default class WorkdirContextPool {
* Access the context mapped to a known directory.
*/
getContext(directory) {
- return this.contexts.get(directory) || nullWorkdirContext;
+ return this.contexts.get(directory) || absentWorkdirContext;
}
add(directory, options = {}) {
From adf8d52d96ce0e964ba214a97e92e13116c08644 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 09:59:36 -0400
Subject: [PATCH 129/161] Begin with an Undetermined context instead of an
Absent one
---
lib/github-package.js | 6 +++---
test/github-package.test.js | 9 ++++++++-
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index f7831be71d..6a5c593a7c 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -7,7 +7,7 @@ import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
import WorkdirCache from './models/workdir-cache';
-import {nullWorkdirContext} from './models/workdir-context';
+import WorkdirContext, {absentWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import StyleCalculator from './models/style-calculator';
@@ -38,7 +38,7 @@ export default class GithubPackage {
this.useLegacyPanels = false;
this.activeContextQueue = new AsyncQueue();
- this.activeContext = nullWorkdirContext;
+ this.activeContext = WorkdirContext.undetermined();
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
@@ -58,7 +58,7 @@ export default class GithubPackage {
}),
this.contextPool.onDidDestroyRepository(context => {
if (context === this.activeContext) {
- this.setActiveContext(nullWorkdirContext);
+ this.setActiveContext(absentWorkdirContext);
}
}),
);
diff --git a/test/github-package.test.js b/test/github-package.test.js
index bec0ddbe82..8aac028f18 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -40,6 +40,13 @@ describe('GithubPackage', function() {
}
describe('activate()', function() {
+ it('begins with an undetermined repository context', async function() {
+ await githubPackage.activate();
+ await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
+
+ assert.isTrue(githubPackage.getActiveRepository().isUndetermined());
+ });
+
it('uses models from preexisting projects', async function() {
const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([
cloneRepository('three-files'),
@@ -55,7 +62,7 @@ describe('GithubPackage', function() {
assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent());
- assert.isTrue(githubPackage.getActiveRepository().isAbsent());
+ assert.isTrue(githubPackage.getActiveRepository().isUndetermined());
});
it('uses an active model from a single preexisting project', async function() {
From 950c14dafad27dd929bd46ec778af1af7acff542 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 10:23:46 -0400
Subject: [PATCH 130/161] Flatten the State hierarchy
---
lib/models/repository-states/absent.js | 13 +------------
lib/models/repository-states/cloning.js | 6 ++----
lib/models/repository-states/initializing.js | 6 ++----
3 files changed, 5 insertions(+), 20 deletions(-)
diff --git a/lib/models/repository-states/absent.js b/lib/models/repository-states/absent.js
index 7f21806f4c..6d3e6914c6 100644
--- a/lib/models/repository-states/absent.js
+++ b/lib/models/repository-states/absent.js
@@ -1,22 +1,11 @@
import State from './state';
-import Empty from './empty';
-export default class Absent extends Empty {
+export default class Absent extends State {
isAbsent() {
return true;
}
- init() {
- return disallowed('init');
- }
-
- clone() {
- return disallowed('clone');
}
}
State.register(Absent);
-
-function disallowed(actionName) {
- return Promise.reject(new Error(`${actionName} cannot be performed on an absent repository`));
-}
diff --git a/lib/models/repository-states/cloning.js b/lib/models/repository-states/cloning.js
index c92d7af0f1..49c374e889 100644
--- a/lib/models/repository-states/cloning.js
+++ b/lib/models/repository-states/cloning.js
@@ -1,9 +1,8 @@
import {mkdirs} from '../../helpers';
import State from './state';
-import Loading from './loading';
-export default class Cloning extends Loading {
+export default class Cloning extends State {
constructor(repository, remoteUrl) {
super(repository);
this.remoteUrl = remoteUrl;
@@ -13,8 +12,7 @@ export default class Cloning extends Loading {
await mkdirs(this.workdir());
await this.git().clone(this.remoteUrl, {recursive: true});
- // See https://github.com/babel/babel/issues/3930
- await Loading.prototype.start.call(this);
+ await this.transitionTo('Loading');
}
}
diff --git a/lib/models/repository-states/initializing.js b/lib/models/repository-states/initializing.js
index f5eb107167..8eddcc6c6e 100644
--- a/lib/models/repository-states/initializing.js
+++ b/lib/models/repository-states/initializing.js
@@ -1,12 +1,10 @@
import State from './state';
-import Loading from './loading';
-export default class Initializing extends Loading {
+export default class Initializing extends State {
async start() {
await this.git().init(this.workdir());
- // See https://github.com/babel/babel/issues/3930
- await Loading.prototype.start.call(this);
+ await this.transitionTo('Loading');
}
}
From c388a4ca4dd3d161144e9da4857858e952a4f735 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 10:24:45 -0400
Subject: [PATCH 131/161] Default implementation of destroy()
---
lib/models/repository-states/empty.js | 2 --
lib/models/repository-states/loading.js | 2 --
lib/models/repository-states/present.js | 4 ----
lib/models/repository-states/state.js | 2 +-
4 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/lib/models/repository-states/empty.js b/lib/models/repository-states/empty.js
index c3f4039dd6..0acf6c05d9 100644
--- a/lib/models/repository-states/empty.js
+++ b/lib/models/repository-states/empty.js
@@ -13,8 +13,6 @@ export default class Empty extends State {
return this.transitionTo('Cloning', remoteUrl);
}
- destroy() {
- this.transitionTo('Destroyed');
}
}
diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js
index 1b9ea13015..df0f113395 100644
--- a/lib/models/repository-states/loading.js
+++ b/lib/models/repository-states/loading.js
@@ -24,8 +24,6 @@ export default class Loading extends State {
await this.repository.clone(remoteUrl);
}
- destroy() {
- this.transitionTo('Destroyed');
}
}
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index ade6925359..e9c02868f3 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -79,10 +79,6 @@ export default class Present extends State {
return true;
}
- destroy() {
- this.transitionTo('Destroyed');
- }
-
refresh() {
this.cache.clear();
this.didUpdate();
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index fae51ea18b..1d7aaf21b5 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -83,7 +83,7 @@ export default class State {
@shouldDelegate
destroy() {
- return unsupportedOperation(this, 'destroy');
+ return this.transitionTo('Destroyed');
}
@shouldDelegate
From 0cc01c32ab995f6040997bf0325c3df4f399e532 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 10:25:35 -0400
Subject: [PATCH 132/161] Use behavior probes instead of state probes
---
lib/models/repository-states/absent.js | 2 ++
lib/models/repository-states/cloning.js | 4 ++++
lib/models/repository-states/empty.js | 2 ++
lib/models/repository-states/initializing.js | 4 ++++
lib/models/repository-states/loading.js | 2 ++
lib/models/repository-states/state.js | 18 ++++++++++++++++++
lib/models/repository-states/undetermined.js | 4 ++++
lib/models/repository.js | 4 ++++
lib/views/git-tab-view.js | 13 ++++++-------
test/models/repository.test.js | 4 +---
10 files changed, 47 insertions(+), 10 deletions(-)
diff --git a/lib/models/repository-states/absent.js b/lib/models/repository-states/absent.js
index 6d3e6914c6..3b5c3f8293 100644
--- a/lib/models/repository-states/absent.js
+++ b/lib/models/repository-states/absent.js
@@ -5,6 +5,8 @@ export default class Absent extends State {
return true;
}
+ showGitTabInit() {
+ return true;
}
}
diff --git a/lib/models/repository-states/cloning.js b/lib/models/repository-states/cloning.js
index 49c374e889..a1eb34223b 100644
--- a/lib/models/repository-states/cloning.js
+++ b/lib/models/repository-states/cloning.js
@@ -14,6 +14,10 @@ export default class Cloning extends State {
await this.transitionTo('Loading');
}
+
+ showGitTabLoading() {
+ return true;
+ }
}
State.register(Cloning);
diff --git a/lib/models/repository-states/empty.js b/lib/models/repository-states/empty.js
index 0acf6c05d9..720973a758 100644
--- a/lib/models/repository-states/empty.js
+++ b/lib/models/repository-states/empty.js
@@ -13,6 +13,8 @@ export default class Empty extends State {
return this.transitionTo('Cloning', remoteUrl);
}
+ showGitTabInit() {
+ return true;
}
}
diff --git a/lib/models/repository-states/initializing.js b/lib/models/repository-states/initializing.js
index 8eddcc6c6e..5d3fa60621 100644
--- a/lib/models/repository-states/initializing.js
+++ b/lib/models/repository-states/initializing.js
@@ -6,6 +6,10 @@ export default class Initializing extends State {
await this.transitionTo('Loading');
}
+
+ showGitTabLoading() {
+ return true;
+ }
}
State.register(Initializing);
diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js
index df0f113395..dd9503faa5 100644
--- a/lib/models/repository-states/loading.js
+++ b/lib/models/repository-states/loading.js
@@ -24,6 +24,8 @@ export default class Loading extends State {
await this.repository.clone(remoteUrl);
}
+ showGitTabLoading() {
+ return true;
}
}
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index 1d7aaf21b5..ae38d4e6d8 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -68,6 +68,24 @@ export default class State {
return false;
}
+ // Behavior probe predicates /////////////////////////////////////////////////////////////////////////////////////////
+ // Determine specific rendering behavior based on the current state.
+
+ @shouldDelegate
+ showGitTabInit() {
+ return false;
+ }
+
+ @shouldDelegate
+ showGitTabInitInProgress() {
+ return false;
+ }
+
+ @shouldDelegate
+ showGitTabLoading() {
+ return false;
+ }
+
// Lifecycle actions /////////////////////////////////////////////////////////////////////////////////////////////////
// These generally default to rejecting a Promise with an error.
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
index 96ebd38bce..c418ca9518 100644
--- a/lib/models/repository-states/undetermined.js
+++ b/lib/models/repository-states/undetermined.js
@@ -9,6 +9,10 @@ export default class Undetermined extends State {
isUndetermined() {
return true;
}
+
+ showGitTabLoading() {
+ return true;
+ }
}
State.register(Undetermined);
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 2daa560db1..abd54fdab1 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -193,6 +193,10 @@ const delegates = [
'isPresent',
'isDestroyed',
+ 'showGitTabInit',
+ 'showGitTabInitInProgress',
+ 'showGitTabLoading',
+
'init',
'clone',
'destroy',
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index b87a1a4848..14f479d5c0 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -15,7 +15,6 @@ export default class GitTabView {
constructor(props) {
this.props = props;
etch.initialize(this);
- this.repoCreationInProgress = false;
this.element.addEventListener('focusin', this.rememberLastFocus);
@@ -35,7 +34,9 @@ export default class GitTabView {
}
render() {
- if (this.props.repository.isEmpty()) {
+ if (this.props.repository.showGitTabInit()) {
+ const inProgress = this.props.repository.showGitTabInitInProgress();
+
return (
@@ -43,14 +44,14 @@ export default class GitTabView {
Initialize this project directory with a Git repository
-
);
} else {
- const isLoading = this.props.fetchInProgress || this.props.repository.isLoading();
+ const isLoading = this.props.fetchInProgress || this.props.repository.showGitTabLoading();
return (
@@ -111,12 +112,10 @@ export default class GitTabView {
@autobind
async initializeRepo(event) {
event.preventDefault();
- this.repoCreationInProgress = true;
etch.update(this);
try {
await this.props.initializeRepo();
} finally {
- this.repoCreationInProgress = false;
etch.update(this);
}
}
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index cbbe505985..2a0528fc33 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -55,9 +55,7 @@ describe('Repository', function() {
await repo.getLoadPromise();
assert.isTrue(repo.isEmpty());
- const initPromise = repo.init();
- assert.isTrue(repo.isLoading());
- await initPromise;
+ await repo.init();
assert.isTrue(repo.isPresent());
assert.equal(repo.getWorkingDirectoryPath(), soonToBeRepositoryPath);
From 7369cacac714d53aabaa87c8ea7727d5cb59bcfe Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 10:25:42 -0400
Subject: [PATCH 133/161] toString() for debugging handiness
---
lib/models/repository.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index abd54fdab1..bd26978b8d 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -147,6 +147,10 @@ export default class Repository {
return this.state.constructor.name === stateName;
}
+ toString() {
+ return `Repository(state=${this.state.constructor.name}, workdir="${this.getWorkingDirectoryPath()}")`;
+ }
+
// Compound Getters //////////////////////////////////////////////////////////////////////////////////////////////////
// Accessor methods for data derived from other, state-provided getters.
From 719c15d715965fb530025388f761139f170ad30e Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 10:36:01 -0400
Subject: [PATCH 134/161] Embiggen Undetermined state timeout
---
lib/models/repository-states/undetermined.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
index c418ca9518..25f1572ba8 100644
--- a/lib/models/repository-states/undetermined.js
+++ b/lib/models/repository-states/undetermined.js
@@ -1,8 +1,10 @@
import State from './state';
+const UNDETERMINED_TIMEOUT = 2000;
+
export default class Undetermined extends State {
start() {
- setTimeout(() => this.transitionTo('Absent'), 1000);
+ setTimeout(() => this.transitionTo('Absent'), UNDETERMINED_TIMEOUT);
return Promise.resolve();
}
From ecefa93c5bec628070c603be03713293a094784a Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 12:48:37 -0400
Subject: [PATCH 135/161] Hide branch and push/pull status bar tiles unless a
repo is present
---
lib/controllers/status-bar-tile-controller.js | 22 ++++++++++++++-----
lib/models/repository-states/present.js | 4 ++++
lib/models/repository-states/state.js | 5 +++++
lib/models/repository.js | 1 +
.../status-bar-tile-controller.test.js | 18 +++++++++++++++
5 files changed, 45 insertions(+), 5 deletions(-)
diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js
index 9ba9e2e08b..c452e6339e 100644
--- a/lib/controllers/status-bar-tile-controller.js
+++ b/lib/controllers/status-bar-tile-controller.js
@@ -88,6 +88,22 @@ export default class StatusBarTileController extends React.Component {
return (
+ {this.renderTiles(repoProps)}
+
+
+ );
+ }
+
+ renderTiles(repoProps) {
+ if (!this.props.repository.showStatusBarTiles()) {
+ return null;
+ }
+
+ return (
+
@@ -141,11 +157,7 @@ export default class StatusBarTileController extends React.Component {
{...repoProps}
/>
-
-
+
);
}
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index e9c02868f3..455f522a64 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -79,6 +79,10 @@ export default class Present extends State {
return true;
}
+ showStatusBarTiles() {
+ return true;
+ }
+
refresh() {
this.cache.clear();
this.didUpdate();
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index ae38d4e6d8..d950125c1f 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -86,6 +86,11 @@ export default class State {
return false;
}
+ @shouldDelegate
+ showStatusBarTiles() {
+ return false;
+ }
+
// Lifecycle actions /////////////////////////////////////////////////////////////////////////////////////////////////
// These generally default to rejecting a Promise with an error.
diff --git a/lib/models/repository.js b/lib/models/repository.js
index bd26978b8d..3164a7a606 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -200,6 +200,7 @@ const delegates = [
'showGitTabInit',
'showGitTabInitInProgress',
'showGitTabLoading',
+ 'showStatusBarTiles',
'init',
'clone',
diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js
index 2895bd9767..81b2a0631c 100644
--- a/test/controllers/status-bar-tile-controller.test.js
+++ b/test/controllers/status-bar-tile-controller.test.js
@@ -6,6 +6,8 @@ import until from 'test-until';
import {mount} from 'enzyme';
import {cloneRepository, buildRepository, setUpLocalAndRemoteRepositories} from '../helpers';
+import {getTempDir} from '../../lib/helpers';
+import Repository from '../../lib/models/repository';
import StatusBarTileController from '../../lib/controllers/status-bar-tile-controller';
import BranchView from '../../lib/views/branch-view';
import PushPullView from '../../lib/views/push-pull-view';
@@ -494,4 +496,20 @@ describe('StatusBarTileController', function() {
assert(toggleGitTab.calledOnce);
});
});
+
+ describe('while the repository is not present', function() {
+ it('does not display the branch or push-pull tiles', async function() {
+ const workdirPath = await getTempDir();
+ const repository = new Repository(workdirPath);
+ assert.isFalse(repository.isPresent());
+
+ const wrapper = mount(React.cloneElement(component, {repository}));
+
+ assert.isFalse(wrapper.find('BranchView').exists());
+ assert.isFalse(wrapper.find('BranchMenuView').exists());
+ assert.isFalse(wrapper.find('PushPullView').exists());
+ assert.isFalse(wrapper.find('PushPullMenuView').exists());
+ assert.isTrue(wrapper.find('ChangedFilesCountView').exists());
+ });
+ });
});
From 06408fa20942daff01cca6d954ea8f98ce277278 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 13:55:53 -0400
Subject: [PATCH 136/161] :fire: unnecessary etch.updates
---
lib/views/git-tab-view.js | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 14f479d5c0..01ecd9034b 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -112,12 +112,7 @@ export default class GitTabView {
@autobind
async initializeRepo(event) {
event.preventDefault();
- etch.update(this);
- try {
- await this.props.initializeRepo();
- } finally {
- etch.update(this);
- }
+ await this.props.initializeRepo();
}
@autobind
From be0ab107b5d64aa33de7e1d10132cdf28e096f67 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 15:13:49 -0400
Subject: [PATCH 137/161] Init dialog
---
lib/views/init-dialog.js | 121 +++++++++++++++++++++++++++++++++
test/views/init-dialog.test.js | 68 ++++++++++++++++++
2 files changed, 189 insertions(+)
create mode 100644 lib/views/init-dialog.js
create mode 100644 test/views/init-dialog.test.js
diff --git a/lib/views/init-dialog.js b/lib/views/init-dialog.js
new file mode 100644
index 0000000000..39a445c1d2
--- /dev/null
+++ b/lib/views/init-dialog.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {autobind} from 'core-decorators';
+import {CompositeDisposable} from 'event-kit';
+
+import Commands, {Command} from './commands';
+
+export default class InitDialog extends React.Component {
+ static propTypes = {
+ config: PropTypes.object.isRequired,
+ commandRegistry: PropTypes.object.isRequired,
+ didAccept: PropTypes.func,
+ didCancel: PropTypes.func,
+ }
+
+ static defaultProps = {
+ didAccept: () => {},
+ didCancel: () => {},
+ }
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ initDisabled: false,
+ };
+
+ this.projectHome = this.props.config.get('core.projectHome');
+ this.subs = new CompositeDisposable();
+ }
+
+ componentDidMount() {
+ if (this.projectPathEditor) {
+ this.projectPathEditor.setText(this.props.config.get('core.projectHome'));
+ this.projectPathModified = false;
+ }
+
+ if (this.projectPathElement) {
+ setTimeout(() => this.projectPathElement.focus());
+ }
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Clone
+
+
+
+ );
+ }
+
+ @autobind
+ init() {
+ if (this.getProjectPath().length === 0) {
+ return;
+ }
+
+ this.props.didAccept(this.getProjectPath());
+ }
+
+ @autobind
+ cancel() {
+ this.props.didCancel();
+ }
+
+ @autobind
+ editorRef() {
+ return element => {
+ if (!element) {
+ return;
+ }
+
+ this.projectPathElement = element;
+ const editor = element.getModel();
+ if (this.projectPathEditor !== editor) {
+ this.projectPathEditor = editor;
+
+ if (this.projectPathSubs) {
+ this.projectPathSubs.dispose();
+ this.subs.remove(this.projectPathSubs);
+ }
+
+ this.projectPathSubs = editor.onDidChange(this.setInitEnablement);
+ this.subs.add(this.projectPathSubs);
+ }
+ };
+ }
+
+ getProjectPath() {
+ return this.projectPathEditor ? this.projectPathEditor.getText() : '';
+ }
+
+ getRemoteUrl() {
+ return this.remoteUrlEditor ? this.remoteUrlEditor.getText() : '';
+ }
+
+ @autobind
+ setInitEnablement() {
+ this.setState({initDisabled: this.getProjectPath().length === 0});
+ }
+}
diff --git a/test/views/init-dialog.test.js b/test/views/init-dialog.test.js
new file mode 100644
index 0000000000..3991558c49
--- /dev/null
+++ b/test/views/init-dialog.test.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import {mount} from 'enzyme';
+import path from 'path';
+
+import InitDialog from '../../lib/views/init-dialog';
+
+describe('InitDialog', function() {
+ let atomEnv, config, commandRegistry;
+ let app, wrapper, didAccept, didCancel;
+
+ beforeEach(function() {
+ atomEnv = global.buildAtomEnvironment();
+ config = atomEnv.config;
+ commandRegistry = atomEnv.commands;
+ sinon.stub(config, 'get').returns(path.join('home', 'me', 'codes'));
+
+ didAccept = sinon.stub();
+ didCancel = sinon.stub();
+
+ app = (
+
+ );
+ wrapper = mount(app);
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ });
+
+ const setTextIn = function(selector, text) {
+ wrapper.find(selector).getDOMNode().getModel().setText(text);
+ };
+
+ it('defaults to your project home path', function() {
+ const text = wrapper.find('atom-text-editor').getDOMNode().getModel().getText();
+ assert.equal(text, path.join('home', 'me', 'codes'));
+ });
+
+ it('disables the initialize button with no project path', function() {
+ setTextIn('.github-ProjectPath atom-text-editor', '');
+
+ assert.isTrue(wrapper.find('button.icon-repo-create').prop('disabled'));
+ });
+
+ it('enables the initialize button when the project path is populated', function() {
+ setTextIn('.github-ProjectPath atom-text-editor', path.join('somewhere', 'else'));
+
+ assert.isFalse(wrapper.find('button.icon-repo-create').prop('disabled'));
+ });
+
+ it('calls the acceptance callback', function() {
+ setTextIn('.github-ProjectPath atom-text-editor', '/somewhere/directory/');
+
+ wrapper.find('button.icon-repo-create').simulate('click');
+
+ assert.isTrue(didAccept.calledWith('/somewhere/directory/'));
+ });
+
+ it('calls the cancellation callback', function() {
+ wrapper.find('button.github-CancelButton').simulate('click');
+ assert.isTrue(didCancel.called);
+ });
+});
From 2981190c80c38df413ec042c418c45ae2485cd79 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 15:15:35 -0400
Subject: [PATCH 138/161] Render the InitDialog when no working directory is
known
---
lib/controllers/root-controller.js | 52 +++++++++++++++-
test/controllers/root-controller.test.js | 77 ++++++++++++++++++++++++
2 files changed, 127 insertions(+), 2 deletions(-)
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index e93cb84b76..664023789c 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -16,6 +16,7 @@ import DockItem from '../views/dock-item';
import Resizer from '../views/resizer';
import Tabs from '../views/tabs';
import CloneDialog from '../views/clone-dialog';
+import InitDialog from '../views/init-dialog';
import CredentialDialog from '../views/credential-dialog';
import Commands, {Command} from '../views/commands';
import GithubTabController from './github-tab-controller';
@@ -82,6 +83,7 @@ export default class RootController extends React.Component {
activeTab: props.savedState.activeTab || 0,
cloneDialogActive: false,
cloneDialogInProgress: false,
+ initDialogActive: false,
credentialDialogQuery: null,
};
@@ -131,6 +133,7 @@ export default class RootController extends React.Component {
{this.renderStatusBarTile()}
{this.renderPanels()}
{(this.state.filePath && this.state.filePatch) ? this.renderFilePatchController() : null}
+ {this.renderInitDialog()}
{this.renderCloneDialog()}
{this.renderCredentialDialog()}
{this.renderRepositoryConflictController()}
@@ -280,6 +283,23 @@ export default class RootController extends React.Component {
);
}
+ renderInitDialog() {
+ if (!this.state.initDialogActive) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ }
+
renderCloneDialog() {
if (!this.state.cloneDialogActive) {
return null;
@@ -338,8 +358,13 @@ export default class RootController extends React.Component {
}
@autobind
- initializeRepo() {
- this.props.createRepositoryForProjectPath(this.props.activeWorkingDirectory);
+ async initializeRepo() {
+ if (this.props.activeWorkingDirectory) {
+ await this.acceptInit(this.props.activeWorkingDirectory);
+ return;
+ }
+
+ this.setState({initDialogActive: true});
}
@autobind
@@ -368,6 +393,29 @@ export default class RootController extends React.Component {
this.setState({cloneDialogActive: false});
}
+ @autobind
+ async acceptInit(projectPath) {
+ try {
+ await this.props.createRepositoryForProjectPath(projectPath);
+ } catch (e) {
+ if (!(e instanceof GitError)) {
+ throw e;
+ } else {
+ this.props.notificationManager.addError(
+ `Unable to initialize git repository in ${projectPath}`,
+ {detail: e.stdErr, dismissable: true},
+ );
+ }
+ } finally {
+ this.setState({initDialogActive: false});
+ }
+ }
+
+ @autobind
+ cancelInit() {
+ this.setState({initDialogActive: false});
+ }
+
@autobind
async showFilePatchForPath(filePath, stagingStatus, {activate, amending} = {}) {
if (!filePath) { return null; }
diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js
index 7c1d87b8d5..bc9170c60a 100644
--- a/test/controllers/root-controller.test.js
+++ b/test/controllers/root-controller.test.js
@@ -360,6 +360,83 @@ describe('RootController', function() {
});
});
+ describe('initializeRepo', function() {
+ let createRepositoryForProjectPath, resolveInit, rejectInit;
+
+ beforeEach(function() {
+ createRepositoryForProjectPath = sinon.stub().returns(new Promise((resolve, reject) => {
+ resolveInit = resolve;
+ rejectInit = reject;
+ }));
+ });
+
+ it('initializes the current working directory if there is one', function() {
+ app = React.cloneElement(app, {
+ createRepositoryForProjectPath,
+ activeWorkingDirectory: '/some/workdir',
+ });
+ const wrapper = shallow(app);
+
+ wrapper.instance().initializeRepo();
+ resolveInit();
+
+ assert.isTrue(createRepositoryForProjectPath.calledWith('/some/workdir'));
+ });
+
+ it('renders the modal init panel', function() {
+ app = React.cloneElement(app, {createRepositoryForProjectPath});
+ const wrapper = shallow(app);
+
+ wrapper.instance().initializeRepo();
+
+ assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('InitDialog'), 1);
+ });
+
+ it('triggers the init callback on accept', function() {
+ app = React.cloneElement(app, {createRepositoryForProjectPath});
+ const wrapper = shallow(app);
+
+ wrapper.instance().initializeRepo();
+ const dialog = wrapper.find('InitDialog');
+ dialog.prop('didAccept')('/a/path');
+ resolveInit();
+
+ assert.isTrue(createRepositoryForProjectPath.calledWith('/a/path'));
+ });
+
+ it('dismisses the init callback on cancel', function() {
+ app = React.cloneElement(app, {createRepositoryForProjectPath});
+ const wrapper = shallow(app);
+
+ wrapper.instance().initializeRepo();
+ const dialog = wrapper.find('InitDialog');
+ dialog.prop('didCancel')();
+
+ assert.isFalse(wrapper.find('InitDialog').exists());
+ });
+
+ it('creates a notification if the init fails', async function() {
+ sinon.stub(notificationManager, 'addError');
+
+ app = React.cloneElement(app, {createRepositoryForProjectPath});
+ const wrapper = shallow(app);
+
+ wrapper.instance().initializeRepo();
+ const dialog = wrapper.find('InitDialog');
+ const acceptPromise = dialog.prop('didAccept')('/a/path');
+ const err = new GitError('git init exited with status 1');
+ err.stdErr = 'this is stderr';
+ rejectInit(err);
+ await acceptPromise;
+
+ assert.isFalse(wrapper.find('InitDialog').exists());
+ assert.isTrue(notificationManager.addError.calledWith(
+ 'Unable to initialize git repository in /a/path',
+ sinon.match({detail: sinon.match(/this is stderr/)}),
+ ));
+ });
+ });
+
describe('github:clone', function() {
let wrapper, cloneRepositoryForProjectPath, resolveClone, rejectClone;
From 076f1d9a3b60298a21e8ea5d261eeec892bd92d4 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 15:20:28 -0400
Subject: [PATCH 139/161] createRepositoryForProjectPath() handles paths
outside of known projects
---
lib/github-package.js | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 57505ed260..90e52ad278 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -6,6 +6,7 @@ import React from 'react';
import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
+import {mkdirs} from './helpers';
import WorkdirCache from './models/workdir-cache';
import WorkdirContext, {absentWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
@@ -247,14 +248,16 @@ export default class GithubPackage {
@autobind
async createRepositoryForProjectPath(projectPath) {
- if (!projectPath) {
- throw new Error('Must specify a project path to create a repository for');
- }
+ await mkdirs(projectPath);
const repository = this.contextPool.add(projectPath).getRepository();
await repository.init();
this.workdirCache.invalidate(projectPath);
+ if (!this.project.contains(projectPath)) {
+ this.project.addPath(projectPath);
+ }
+
await this.scheduleActiveContextUpdate();
}
From 48911858ce5c6934ef875a25e433e83692b3ad07 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 15:38:59 -0400
Subject: [PATCH 140/161] Better errors when attempting to init an existing git
repository
---
lib/controllers/root-controller.js | 21 ++++++++-------------
lib/models/repository-states/present.js | 14 ++++++++++++++
2 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index 664023789c..6a2c88c527 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -378,11 +378,10 @@ export default class RootController extends React.Component {
try {
await this.props.cloneRepositoryForProjectPath(remoteUrl, projectPath);
} catch (e) {
- if (!(e instanceof GitError)) {
- throw e;
- } else {
- this.props.notificationManager.addError(`Unable to clone ${remoteUrl}`, {detail: e.stdErr, dismissable: true});
- }
+ this.props.notificationManager.addError(
+ `Unable to clone ${remoteUrl}`,
+ {detail: e.stdErr, dismissable: true},
+ );
} finally {
this.setState({cloneDialogInProgress: false, cloneDialogActive: false});
}
@@ -398,14 +397,10 @@ export default class RootController extends React.Component {
try {
await this.props.createRepositoryForProjectPath(projectPath);
} catch (e) {
- if (!(e instanceof GitError)) {
- throw e;
- } else {
- this.props.notificationManager.addError(
- `Unable to initialize git repository in ${projectPath}`,
- {detail: e.stdErr, dismissable: true},
- );
- }
+ this.props.notificationManager.addError(
+ `Unable to initialize git repository in ${projectPath}`,
+ {detail: e.stdErr, dismissable: true},
+ );
} finally {
this.setState({initDialogActive: false});
}
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index 455f522a64..c629f2e966 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -88,6 +88,20 @@ export default class Present extends State {
this.didUpdate();
}
+ init() {
+ return super.init().catch(e => {
+ e.stdErr = 'This directory already contains a git repository';
+ return Promise.reject(e);
+ });
+ }
+
+ clone() {
+ return super.clone().catch(e => {
+ e.stdErr = 'This directory already contains a git repository';
+ return Promise.reject(e);
+ });
+ }
+
// Git operations ////////////////////////////////////////////////////////////////////////////////////////////////////
// Staging and unstaging
From c66acdba632a649c5fc31e65d20e148febe62cd0 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Wed, 19 Apr 2017 15:53:00 -0400
Subject: [PATCH 141/161] Change the init message based on context
---
lib/models/repository-states/absent.js | 4 ++++
lib/models/repository-states/state.js | 5 +++++
lib/models/repository-states/undetermined.js | 4 ++++
lib/models/repository.js | 1 +
lib/views/git-tab-view.js | 5 ++++-
5 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/lib/models/repository-states/absent.js b/lib/models/repository-states/absent.js
index 3b5c3f8293..700dc27666 100644
--- a/lib/models/repository-states/absent.js
+++ b/lib/models/repository-states/absent.js
@@ -8,6 +8,10 @@ export default class Absent extends State {
showGitTabInit() {
return true;
}
+
+ hasDirectory() {
+ return false;
+ }
}
State.register(Absent);
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index d950125c1f..657cd04136 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -91,6 +91,11 @@ export default class State {
return false;
}
+ @shouldDelegate
+ hasDirectory() {
+ return true;
+ }
+
// Lifecycle actions /////////////////////////////////////////////////////////////////////////////////////////////////
// These generally default to rejecting a Promise with an error.
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
index 25f1572ba8..d095b286f1 100644
--- a/lib/models/repository-states/undetermined.js
+++ b/lib/models/repository-states/undetermined.js
@@ -15,6 +15,10 @@ export default class Undetermined extends State {
showGitTabLoading() {
return true;
}
+
+ hasDirectory() {
+ return false;
+ }
}
State.register(Undetermined);
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 3164a7a606..e5f0e55d2e 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -201,6 +201,7 @@ const delegates = [
'showGitTabInitInProgress',
'showGitTabLoading',
'showStatusBarTiles',
+ 'hasDirectory',
'init',
'clone',
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 01ecd9034b..4e11e30d61 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -36,6 +36,9 @@ export default class GitTabView {
render() {
if (this.props.repository.showGitTabInit()) {
const inProgress = this.props.repository.showGitTabInitInProgress();
+ const message = this.props.repository.hasDirectory() ?
+ 'Initialize this project directory with a git repository' :
+ 'Initialize a new project directory with a git repository';
return (
@@ -43,7 +46,7 @@ export default class GitTabView {
-
Initialize this project directory with a Git repository
+
{message}
{inProgress ? 'Creating repository...' : 'Create repository'}
From 9b24ba4361295e33edae2499b5b5b1c072ca9c0b Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 11:35:11 -0400
Subject: [PATCH 142/161] Introduce behavioral variants of the Undetermined
state
---
lib/models/repository-states/undetermined.js | 15 +++++++++------
test/models/repository.test.js | 16 +++++++++++++++-
2 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
index d095b286f1..25cd26bbaa 100644
--- a/lib/models/repository-states/undetermined.js
+++ b/lib/models/repository-states/undetermined.js
@@ -1,11 +1,10 @@
import State from './state';
-const UNDETERMINED_TIMEOUT = 2000;
-
export default class Undetermined extends State {
- start() {
- setTimeout(() => this.transitionTo('Absent'), UNDETERMINED_TIMEOUT);
- return Promise.resolve();
+ constructor(repository, loadingLike) {
+ super(repository);
+
+ this.loadingLike = loadingLike;
}
isUndetermined() {
@@ -13,7 +12,11 @@ export default class Undetermined extends State {
}
showGitTabLoading() {
- return true;
+ return this.loadingLike;
+ }
+
+ showGitTabInit() {
+ return !this.loadingLike;
}
hasDirectory() {
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index 2a0528fc33..464d634425 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -4,7 +4,7 @@ import dedent from 'dedent-js';
import temp from 'temp';
import util from 'util';
-import Repository from '../../lib/models/repository';
+import Repository, {ABSENTLIKE, LOADINGLIKE} from '../../lib/models/repository';
import {expectedDelegates} from '../../lib/models/repository-states';
import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers';
@@ -44,6 +44,20 @@ describe('Repository', function() {
repository = Repository.absent();
assert.isTrue(repository.isInState('Absent'));
});
+
+ it('begins in an absentlike Undetermined state with .undetermined(ABSENTLIKE)', function() {
+ repository = Repository.undetermined(ABSENTLIKE);
+ assert.isTrue(repository.isInState('Undetermined'));
+ assert.isFalse(repository.showGitTabLoading());
+ assert.isTrue(repository.showGitTabInit());
+ });
+
+ it('begins in a loadinglike Undetermined state with .undetermined(LOADINGLIKE)', function() {
+ repository = Repository.undetermined(LOADINGLIKE);
+ assert.isTrue(repository.isInState('Undetermined'));
+ assert.isTrue(repository.showGitTabLoading());
+ assert.isFalse(repository.showGitTabInit());
+ });
});
describe('init', function() {
From e7807d34ffb79298d66b0e83fb0248f0c18cc6b1 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 11:35:40 -0400
Subject: [PATCH 143/161] Create undetermined WorkdirContexts with a behavioral
variant
---
lib/models/repository.js | 13 +++++++++----
lib/models/workdir-context.js | 11 +++++++----
test/models/workdir-context.test.js | 15 ++++++++++++---
3 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index e5f0e55d2e..6bfef6103d 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -9,10 +9,15 @@ import {LoadingState, AbsentState, UndeterminedState} from './repository-states'
const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
+// Internal tokens used to designate the initial state of a Repository.
const ABSENT = {};
const UNDETERMINED = {};
const LOADING = {};
+// Tokens used to control the behavior of the Undetermined state.
+export const ABSENTLIKE = {};
+export const LOADINGLIKE = {};
+
export class AbortMergeError extends Error {
constructor(code, filePath) {
super();
@@ -43,7 +48,7 @@ function getStateConstructor(token) {
}
export default class Repository {
- constructor(workingDirectoryPath, gitStrategy = null, initialState = LOADING) {
+ constructor(workingDirectoryPath, gitStrategy = null, initialState = LOADING, ...stateArgs) {
this.workingDirectoryPath = workingDirectoryPath;
this.git = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
@@ -60,15 +65,15 @@ export default class Repository {
});
});
- this.transitionTo(getStateConstructor(initialState));
+ this.transitionTo(getStateConstructor(initialState), ...stateArgs);
}
static absent() {
return new Repository(null, null, ABSENT);
}
- static undetermined() {
- return new Repository(null, null, UNDETERMINED);
+ static undetermined(behavior) {
+ return new Repository(null, null, UNDETERMINED, behavior === LOADINGLIKE);
}
// State management //////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index c4d1c1a1c9..11c9ebbf05 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -1,12 +1,15 @@
import {Emitter, CompositeDisposable} from 'event-kit';
import {autobind} from 'core-decorators';
-import Repository from './repository';
+import Repository, {ABSENTLIKE, LOADINGLIKE} from './repository';
import ResolutionProgress from './conflicts/resolution-progress';
import FileSystemChangeObserver from './file-system-change-observer';
import WorkspaceChangeObserver from './workspace-change-observer';
+export {ABSENTLIKE, LOADINGLIKE};
+
const initRepoState = Symbol('initRepoState');
+const initRepoArg = Symbol('initRepoArgs');
const ABSENT = {};
const UNDETERMINED = {};
@@ -32,7 +35,7 @@ export default class WorkdirContext {
this.directory = directory;
this.repository = (token => {
if (token === ABSENT) { return Repository.absent(); }
- if (token === UNDETERMINED) { return Repository.undetermined(); }
+ if (token === UNDETERMINED) { return Repository.undetermined(options[initRepoArg]); }
return new Repository(directory);
})(options[initRepoState]);
@@ -61,8 +64,8 @@ export default class WorkdirContext {
this.repositoryChangedState({from: null, to: this.repository.state});
}
- static undetermined() {
- return new WorkdirContext(null, {[initRepoState]: UNDETERMINED});
+ static undetermined(behavior) {
+ return new WorkdirContext(null, {[initRepoState]: UNDETERMINED, [initRepoArg]: behavior});
}
/**
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 47edba2829..088719ce1f 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -2,7 +2,7 @@ import {CompositeDisposable, Disposable} from 'event-kit';
import {cloneRepository, sha} from '../helpers';
-import WorkdirContext, {absentWorkdirContext} from '../../lib/models/workdir-context';
+import WorkdirContext, {absentWorkdirContext, ABSENTLIKE, LOADINGLIKE} from '../../lib/models/workdir-context';
describe('WorkdirContext', function() {
let context, workingDirectory, subs;
@@ -127,8 +127,17 @@ describe('WorkdirContext', function() {
assert.isTrue(absentWorkdirContext.getRepository().isAbsent());
});
- it('can be constructed containing a Repository in an undetermined state', function() {
- const undetermined = WorkdirContext.undetermined();
+ it('can be constructed containing an undetermined Repository that acts absent', function() {
+ const undetermined = WorkdirContext.undetermined(ABSENTLIKE);
assert.isTrue(undetermined.getRepository().isUndetermined());
+ assert.isFalse(undetermined.getRepository().showGitTabLoading());
+ assert.isTrue(undetermined.getRepository().showGitTabInit());
+ });
+
+ it("can be constructed containing an undetermined Repository that acts like it's loading", function() {
+ const undetermined = WorkdirContext.undetermined(LOADINGLIKE);
+ assert.isTrue(undetermined.getRepository().isUndetermined());
+ assert.isTrue(undetermined.getRepository().showGitTabLoading());
+ assert.isFalse(undetermined.getRepository().showGitTabInit());
});
});
From 29416d060318948c87de44aeb8d8543c6c594e82 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 11:36:59 -0400
Subject: [PATCH 144/161] Heuristically derive the initial context
---
lib/github-package.js | 23 ++++++++--
test/github-package.test.js | 88 ++++++++++++++++++++++++++++++++++---
2 files changed, 101 insertions(+), 10 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 90e52ad278..2349c70ec3 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -8,7 +8,7 @@ import {autobind} from 'core-decorators';
import {mkdirs} from './helpers';
import WorkdirCache from './models/workdir-cache';
-import WorkdirContext, {absentWorkdirContext} from './models/workdir-context';
+import WorkdirContext, {absentWorkdirContext, LOADINGLIKE, ABSENTLIKE} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import StyleCalculator from './models/style-calculator';
@@ -25,7 +25,8 @@ const defaultState = {
};
export default class GithubPackage {
- constructor(workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm) {
+ constructor(workspace, project, commandRegistry, notificationManager, tooltips, styles, config,
+ confirm, getLoadSettings) {
this.workspace = workspace;
this.project = project;
this.commandRegistry = commandRegistry;
@@ -39,7 +40,7 @@ export default class GithubPackage {
this.useLegacyPanels = false;
this.activeContextQueue = new AsyncQueue();
- this.activeContext = WorkdirContext.undetermined();
+ this.activeContext = this.getInitialContext(getLoadSettings());
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
@@ -67,6 +68,22 @@ export default class GithubPackage {
this.setupYardstick();
}
+ /**
+ * Synchronously determine the initial WorkdirContext of a newly constructed package instance based on:
+ * - the number of projects already loaded into the workspace
+ * - any initial paths specified in the load settings
+ */
+ getInitialContext(loadSettings) {
+ const projectPathCount = this.project.getPaths().length;
+ const initLocationCount = (loadSettings.locationsToOpen || []).length;
+
+ if (projectPathCount === 1 || (projectPathCount === 0 && initLocationCount === 1)) {
+ return WorkdirContext.undetermined(LOADINGLIKE);
+ } else {
+ return WorkdirContext.undetermined(ABSENTLIKE);
+ }
+ }
+
setupYardstick() {
const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk'];
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 8aac028f18..1a6791af6d 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -9,6 +9,7 @@ import GithubPackage from '../lib/github-package';
describe('GithubPackage', function() {
let atomEnv, workspace, project, commandRegistry, notificationManager, config, confirm, tooltips, styles;
+ let getLoadSettings;
let githubPackage, contextPool;
beforeEach(function() {
@@ -21,8 +22,10 @@ describe('GithubPackage', function() {
config = atomEnv.config;
confirm = atomEnv.confirm.bind(atomEnv);
styles = atomEnv.styles;
+ getLoadSettings = atomEnv.getLoadSettings.bind(atomEnv);
+
githubPackage = new GithubPackage(
- workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm,
+ workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm, getLoadSettings,
);
contextPool = githubPackage.getContextPool();
@@ -39,6 +42,80 @@ describe('GithubPackage', function() {
return updatePromise;
}
+ describe('construction', function() {
+ let githubPackage1;
+
+ afterEach(async function() {
+ if (githubPackage1) {
+ await githubPackage1.deactivate();
+ }
+ });
+
+ async function constructWith(projectPaths, initialLocations) {
+ const realProjectPaths = await Promise.all(
+ projectPaths.map(projectPath => getTempDir(projectPath)),
+ );
+
+ project.setPaths(realProjectPaths);
+ const getLoadSettings1 = () => ({
+ locationsToOpen: initialLocations.map(location => ({pathToOpen: location})),
+ });
+
+ githubPackage1 = new GithubPackage(
+ workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm, getLoadSettings1,
+ );
+ }
+
+ function assertAbsentLike() {
+ const repository = githubPackage1.getActiveRepository();
+ assert.isTrue(repository.isUndetermined());
+ assert.isFalse(repository.showGitTabLoading());
+ assert.isTrue(repository.showGitTabInit());
+ }
+
+ function assertLoadingLike() {
+ const repository = githubPackage1.getActiveRepository();
+ assert.isTrue(repository.isUndetermined());
+ assert.isTrue(repository.showGitTabLoading());
+ assert.isFalse(repository.showGitTabInit());
+ }
+
+ it('with no projects or initial locations begins with an absent-like undetermined context', async function() {
+ await constructWith([], []);
+ assertAbsentLike();
+ });
+
+ it('with one existing project begins with a loading-like undetermined context', async function() {
+ await constructWith(['one'], []);
+ assertLoadingLike();
+ });
+
+ it('with several existing projects begins with an absent-like undetermined context', async function() {
+ await constructWith(['one', 'two'], []);
+ assertAbsentLike();
+ });
+
+ it('with no projects but one initial location begins with a loading-like undetermined context', async function() {
+ await constructWith([], ['one']);
+ assertLoadingLike();
+ });
+
+ it('with no projects and several initial locations begins with an absent-like undetermined context', async function() {
+ await constructWith([], ['one', 'two']);
+ assertAbsentLike();
+ });
+
+ it('with one project and initial locations begins with a loading-like undetermined context', async function() {
+ await constructWith(['one'], ['two', 'three']);
+ assertLoadingLike();
+ });
+
+ it('with several projects and an initial location begins with an absent-like undetermined context', async function() {
+ await constructWith(['one', 'two'], ['three']);
+ assertAbsentLike();
+ });
+ });
+
describe('activate()', function() {
it('begins with an undetermined repository context', async function() {
await githubPackage.activate();
@@ -270,12 +347,9 @@ describe('GithubPackage', function() {
await githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
const repository2 = contextPool.getContext(workdirPath2).getRepository();
+ assert.isTrue(githubPackage.getActiveRepository().isUndetermined());
- assert.isTrue(githubPackage.getActiveRepository().isAbsent());
-
- const updatePromise = githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise();
- workspace.open(path.join(workdirPath2, 'b.txt'));
- await updatePromise;
+ await contextUpdateAfter(() => workspace.open(path.join(workdirPath2, 'b.txt')));
assert.strictEqual(githubPackage.getActiveRepository(), repository2);
});
@@ -560,7 +634,7 @@ describe('GithubPackage', function() {
assert.isUndefined(payload.resolutionProgressByPath[workdirNoConflict]);
const githubPackage1 = new GithubPackage(
- workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm,
+ workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm, getLoadSettings,
);
await githubPackage1.activate(payload);
await githubPackage1.scheduleActiveContextUpdate();
From 0903db0a489e92b7b1de6d44c5509e908c001b7f Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 11:37:29 -0400
Subject: [PATCH 145/161] Pass getLoadSettings to the package
---
lib/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/index.js b/lib/index.js
index 4b0ed0033b..2387bc5b8b 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -5,7 +5,7 @@ const entry = {
initialize() {
pack = new GithubPackage(
atom.workspace, atom.project, atom.commands, atom.notifications, atom.tooltips,
- atom.styles, atom.config, atom.confirm.bind(atom),
+ atom.styles, atom.config, atom.confirm.bind(atom), atom.getLoadSettings.bind(atom),
);
},
};
From d07e864c12ef633c1d1059be169c63c9a159dee3 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 11:51:14 -0400
Subject: [PATCH 146/161] Use initialPaths instead of locationsToOpen
---
lib/github-package.js | 4 ++--
test/github-package.test.js | 16 +++++++---------
2 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 2349c70ec3..0ba801df7e 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -75,9 +75,9 @@ export default class GithubPackage {
*/
getInitialContext(loadSettings) {
const projectPathCount = this.project.getPaths().length;
- const initLocationCount = (loadSettings.locationsToOpen || []).length;
+ const initPathCount = (loadSettings.initialPaths || []).length;
- if (projectPathCount === 1 || (projectPathCount === 0 && initLocationCount === 1)) {
+ if (projectPathCount === 1 || (projectPathCount === 0 && initPathCount === 1)) {
return WorkdirContext.undetermined(LOADINGLIKE);
} else {
return WorkdirContext.undetermined(ABSENTLIKE);
diff --git a/test/github-package.test.js b/test/github-package.test.js
index 1a6791af6d..9adbf7f469 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -51,15 +51,13 @@ describe('GithubPackage', function() {
}
});
- async function constructWith(projectPaths, initialLocations) {
+ async function constructWith(projectPaths, initialPaths) {
const realProjectPaths = await Promise.all(
projectPaths.map(projectPath => getTempDir(projectPath)),
);
project.setPaths(realProjectPaths);
- const getLoadSettings1 = () => ({
- locationsToOpen: initialLocations.map(location => ({pathToOpen: location})),
- });
+ const getLoadSettings1 = () => ({initialPaths});
githubPackage1 = new GithubPackage(
workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm, getLoadSettings1,
@@ -80,7 +78,7 @@ describe('GithubPackage', function() {
assert.isFalse(repository.showGitTabInit());
}
- it('with no projects or initial locations begins with an absent-like undetermined context', async function() {
+ it('with no projects or initial paths begins with an absent-like undetermined context', async function() {
await constructWith([], []);
assertAbsentLike();
});
@@ -95,22 +93,22 @@ describe('GithubPackage', function() {
assertAbsentLike();
});
- it('with no projects but one initial location begins with a loading-like undetermined context', async function() {
+ it('with no projects but one initial path begins with a loading-like undetermined context', async function() {
await constructWith([], ['one']);
assertLoadingLike();
});
- it('with no projects and several initial locations begins with an absent-like undetermined context', async function() {
+ it('with no projects and several initial paths begins with an absent-like undetermined context', async function() {
await constructWith([], ['one', 'two']);
assertAbsentLike();
});
- it('with one project and initial locations begins with a loading-like undetermined context', async function() {
+ it('with one project and initial paths begins with a loading-like undetermined context', async function() {
await constructWith(['one'], ['two', 'three']);
assertLoadingLike();
});
- it('with several projects and an initial location begins with an absent-like undetermined context', async function() {
+ it('with several projects and an initial path begins with an absent-like undetermined context', async function() {
await constructWith(['one', 'two'], ['three']);
assertAbsentLike();
});
From bf2b039d6639cdc735107ae9cbfb9f5966f4c849 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 11:51:46 -0400
Subject: [PATCH 147/161] Account for renders that occur while fetching Promise
data
---
test/controllers/git-tab-controller.test.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js
index 0a28c544e1..93b578dbb3 100644
--- a/test/controllers/git-tab-controller.test.js
+++ b/test/controllers/git-tab-controller.test.js
@@ -471,7 +471,7 @@ describe('GitTabController', function() {
await assert.async.isDefined(controller.refs.gitTab.refs.stagingView);
const stagingView = controller.refs.gitTab.refs.stagingView;
- assert.equal(stagingView.props.mergeConflicts.length, 5);
+ await assert.async.equal(stagingView.props.mergeConflicts.length, 5);
assert.equal(stagingView.props.stagedChanges.length, 0);
const conflict1 = stagingView.props.mergeConflicts.filter(c => c.filePath === 'modified-on-both-ours.txt')[0];
@@ -522,7 +522,7 @@ describe('GitTabController', function() {
await assert.async.isDefined(controller.refs.gitTab.refs.stagingView);
const stagingView = controller.refs.gitTab.refs.stagingView;
- assert.lengthOf(stagingView.props.unstagedChanges, 2);
+ await assert.async.lengthOf(stagingView.props.unstagedChanges, 2);
// ensure staging the same file twice does not cause issues
// second stage action is a no-op since the first staging operation is in flight
From d63964809899b1758372777f3cc2735b43c89558 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 13:46:49 -0400
Subject: [PATCH 148/161] Split Undetermined into LoadingGuess and AbsentGuess
---
lib/models/repository-states/absent-guess.js | 25 +++++++++++++++++
lib/models/repository-states/loading-guess.js | 25 +++++++++++++++++
lib/models/repository-states/state.js | 12 ++++++++-
lib/models/repository-states/undetermined.js | 27 -------------------
lib/models/repository.js | 4 ++-
5 files changed, 64 insertions(+), 29 deletions(-)
create mode 100644 lib/models/repository-states/absent-guess.js
create mode 100644 lib/models/repository-states/loading-guess.js
delete mode 100644 lib/models/repository-states/undetermined.js
diff --git a/lib/models/repository-states/absent-guess.js b/lib/models/repository-states/absent-guess.js
new file mode 100644
index 0000000000..c253ccc084
--- /dev/null
+++ b/lib/models/repository-states/absent-guess.js
@@ -0,0 +1,25 @@
+import State from './state';
+
+export default class AbsentGuess extends State {
+ isAbsentGuess() {
+ return true;
+ }
+
+ isUndetermined() {
+ return true;
+ }
+
+ showGitTabLoading() {
+ return false;
+ }
+
+ showGitTabInit() {
+ return true;
+ }
+
+ hasDirectory() {
+ return false;
+ }
+}
+
+State.register(AbsentGuess);
diff --git a/lib/models/repository-states/loading-guess.js b/lib/models/repository-states/loading-guess.js
new file mode 100644
index 0000000000..31cfdbc7a0
--- /dev/null
+++ b/lib/models/repository-states/loading-guess.js
@@ -0,0 +1,25 @@
+import State from './state';
+
+export default class LoadingGuess extends State {
+ isLoadingGuess() {
+ return true;
+ }
+
+ isUndetermined() {
+ return true;
+ }
+
+ showGitTabLoading() {
+ return true;
+ }
+
+ showGitTabInit() {
+ return false;
+ }
+
+ hasDirectory() {
+ return false;
+ }
+}
+
+State.register(LoadingGuess);
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index 657cd04136..125e4dbd3e 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -39,7 +39,12 @@ export default class State {
// Allow external callers to identify which state a Repository is in if necessary.
@shouldDelegate
- isUndetermined() {
+ isLoadingGuess() {
+ return false;
+ }
+
+ @shouldDelegate
+ isAbsentGuess() {
return false;
}
@@ -71,6 +76,11 @@ export default class State {
// Behavior probe predicates /////////////////////////////////////////////////////////////////////////////////////////
// Determine specific rendering behavior based on the current state.
+ @shouldDelegate
+ isUndetermined() {
+ return false;
+ }
+
@shouldDelegate
showGitTabInit() {
return false;
diff --git a/lib/models/repository-states/undetermined.js b/lib/models/repository-states/undetermined.js
deleted file mode 100644
index 25cd26bbaa..0000000000
--- a/lib/models/repository-states/undetermined.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import State from './state';
-
-export default class Undetermined extends State {
- constructor(repository, loadingLike) {
- super(repository);
-
- this.loadingLike = loadingLike;
- }
-
- isUndetermined() {
- return true;
- }
-
- showGitTabLoading() {
- return this.loadingLike;
- }
-
- showGitTabInit() {
- return !this.loadingLike;
- }
-
- hasDirectory() {
- return false;
- }
-}
-
-State.register(Undetermined);
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 6bfef6103d..e316d9a1b3 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -195,13 +195,15 @@ export default class Repository {
// This list should match the methods decorated with @shouldDelegate in `lib/models/repository-states/state.js`. A test
// case in `test/models/repository.test.js` ensures that these sets match.
const delegates = [
- 'isUndetermined',
+ 'isLoadingGuess',
+ 'isAbsentGuess',
'isAbsent',
'isLoading',
'isEmpty',
'isPresent',
'isDestroyed',
+ 'isUndetermined',
'showGitTabInit',
'showGitTabInitInProgress',
'showGitTabLoading',
From 46b2aa31d23c0456cabeed0b2d19e660c9d1c6af Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 13:47:28 -0400
Subject: [PATCH 149/161] Export state constructors as-is
---
lib/models/repository-states/index.js | 8 +++---
lib/models/repository.js | 36 +++++++++------------------
2 files changed, 17 insertions(+), 27 deletions(-)
diff --git a/lib/models/repository-states/index.js b/lib/models/repository-states/index.js
index e4ed4ded2c..7637057b16 100644
--- a/lib/models/repository-states/index.js
+++ b/lib/models/repository-states/index.js
@@ -1,8 +1,10 @@
export {expectedDelegates} from './state';
-export {default as LoadingState} from './loading';
-export {default as UndeterminedState} from './undetermined';
-export {default as AbsentState} from './absent';
+// Load and export possible initial states
+export {default as Loading} from './loading';
+export {default as LoadingGuess} from './loading-guess';
+export {default as Absent} from './absent';
+export {default as AbsentGuess} from './absent-guess';
// Load and register remaining states
import './empty';
diff --git a/lib/models/repository.js b/lib/models/repository.js
index e316d9a1b3..1c19776993 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -5,18 +5,12 @@ import {Emitter} from 'event-kit';
import CompositeGitStrategy from '../composite-git-strategy';
import {readFile} from '../helpers';
import Remote, {nullRemote} from './remote';
-import {LoadingState, AbsentState, UndeterminedState} from './repository-states';
+import {Loading, Absent, LoadingGuess, AbsentGuess} from './repository-states';
const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m;
-// Internal tokens used to designate the initial state of a Repository.
-const ABSENT = {};
-const UNDETERMINED = {};
-const LOADING = {};
-
-// Tokens used to control the behavior of the Undetermined state.
-export const ABSENTLIKE = {};
-export const LOADINGLIKE = {};
+// Internal option keys used to designate the desired initial state of a Repository.
+const initialStateSym = Symbol('initialState');
export class AbortMergeError extends Error {
constructor(code, filePath) {
@@ -37,18 +31,8 @@ export class CommitError extends Error {
}
}
-function getStateConstructor(token) {
- if (token === ABSENT) {
- return AbsentState;
- } else if (token === UNDETERMINED) {
- return UndeterminedState;
- } else {
- return LoadingState;
- }
-}
-
export default class Repository {
- constructor(workingDirectoryPath, gitStrategy = null, initialState = LOADING, ...stateArgs) {
+ constructor(workingDirectoryPath, gitStrategy = null, options = {}) {
this.workingDirectoryPath = workingDirectoryPath;
this.git = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
@@ -65,15 +49,19 @@ export default class Repository {
});
});
- this.transitionTo(getStateConstructor(initialState), ...stateArgs);
+ this.transitionTo(options[initialStateSym] || Loading);
}
static absent() {
- return new Repository(null, null, ABSENT);
+ return new Repository(null, null, {[initialStateSym]: Absent});
+ }
+
+ static loadingGuess() {
+ return new Repository(null, null, {[initialStateSym]: LoadingGuess});
}
- static undetermined(behavior) {
- return new Repository(null, null, UNDETERMINED, behavior === LOADINGLIKE);
+ static absentGuess() {
+ return new Repository(null, null, {[initialStateSym]: AbsentGuess});
}
// State management //////////////////////////////////////////////////////////////////////////////////////////////////
From dbe22c4f951231109cde131c2114fc9210b40180 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 13:47:45 -0400
Subject: [PATCH 150/161] Move initial context guess into WorkdirContext
---
lib/github-package.js | 25 +++++++------------------
lib/models/workdir-context.js | 29 +++++++++++++----------------
2 files changed, 20 insertions(+), 34 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index 0ba801df7e..c14115f095 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -8,7 +8,7 @@ import {autobind} from 'core-decorators';
import {mkdirs} from './helpers';
import WorkdirCache from './models/workdir-cache';
-import WorkdirContext, {absentWorkdirContext, LOADINGLIKE, ABSENTLIKE} from './models/workdir-context';
+import WorkdirContext, {absentWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import StyleCalculator from './models/style-calculator';
@@ -39,8 +39,13 @@ export default class GithubPackage {
this.confirm = confirm;
this.useLegacyPanels = false;
+ const criteria = {
+ projectPathCount: this.project.getPaths().length,
+ initPathCount: (getLoadSettings().initialPaths || []) || 0,
+ };
+
this.activeContextQueue = new AsyncQueue();
- this.activeContext = this.getInitialContext(getLoadSettings());
+ this.activeContext = WorkdirContext.guess(criteria);
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
@@ -68,22 +73,6 @@ export default class GithubPackage {
this.setupYardstick();
}
- /**
- * Synchronously determine the initial WorkdirContext of a newly constructed package instance based on:
- * - the number of projects already loaded into the workspace
- * - any initial paths specified in the load settings
- */
- getInitialContext(loadSettings) {
- const projectPathCount = this.project.getPaths().length;
- const initPathCount = (loadSettings.initialPaths || []).length;
-
- if (projectPathCount === 1 || (projectPathCount === 0 && initPathCount === 1)) {
- return WorkdirContext.undetermined(LOADINGLIKE);
- } else {
- return WorkdirContext.undetermined(ABSENTLIKE);
- }
- }
-
setupYardstick() {
const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk'];
diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js
index 11c9ebbf05..3d796a7829 100644
--- a/lib/models/workdir-context.js
+++ b/lib/models/workdir-context.js
@@ -1,18 +1,12 @@
import {Emitter, CompositeDisposable} from 'event-kit';
import {autobind} from 'core-decorators';
-import Repository, {ABSENTLIKE, LOADINGLIKE} from './repository';
+import Repository from './repository';
import ResolutionProgress from './conflicts/resolution-progress';
import FileSystemChangeObserver from './file-system-change-observer';
import WorkspaceChangeObserver from './workspace-change-observer';
-export {ABSENTLIKE, LOADINGLIKE};
-
-const initRepoState = Symbol('initRepoState');
-const initRepoArg = Symbol('initRepoArgs');
-
-const ABSENT = {};
-const UNDETERMINED = {};
+const createRepoSym = Symbol('createRepo');
/*
* Bundle of model objects associated with a git working directory.
@@ -33,11 +27,7 @@ export default class WorkdirContext {
*/
constructor(directory, options = {}) {
this.directory = directory;
- this.repository = (token => {
- if (token === ABSENT) { return Repository.absent(); }
- if (token === UNDETERMINED) { return Repository.undetermined(options[initRepoArg]); }
- return new Repository(directory);
- })(options[initRepoState]);
+ this.repository = (options[createRepoSym] || (() => new Repository(directory)))();
const {window: theWindow, workspace, promptCallback} = options;
this.savedResolutionProgress = options.savedResolutionProgress || {};
@@ -64,8 +54,15 @@ export default class WorkdirContext {
this.repositoryChangedState({from: null, to: this.repository.state});
}
- static undetermined(behavior) {
- return new WorkdirContext(null, {[initRepoState]: UNDETERMINED, [initRepoArg]: behavior});
+ static guess(options) {
+ const projectPathCount = options.projectPathCount || 0;
+ const initPathCount = options.initPathCount || 0;
+
+ const createRepo = (projectPathCount === 1 || (projectPathCount === 0 && initPathCount === 1)) ?
+ () => Repository.loadingGuess() :
+ () => Repository.absentGuess();
+
+ return new WorkdirContext(null, {[createRepoSym]: createRepo});
}
/**
@@ -189,7 +186,7 @@ export default class WorkdirContext {
class AbsentWorkdirContext extends WorkdirContext {
constructor() {
- super(null, {[initRepoState]: ABSENT});
+ super(null, {[createRepoSym]: () => Repository.absent()});
}
isPresent() {
From 0e5763320c6b07a0e23d487d88451233d0bd7fb0 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 13:53:54 -0400
Subject: [PATCH 151/161] Update tests to use static constructor methods
---
lib/github-package.js | 2 +-
test/models/repository.test.js | 14 +++++++-------
test/models/workdir-context.test.js | 6 +++---
3 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/lib/github-package.js b/lib/github-package.js
index c14115f095..772f084253 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -41,7 +41,7 @@ export default class GithubPackage {
const criteria = {
projectPathCount: this.project.getPaths().length,
- initPathCount: (getLoadSettings().initialPaths || []) || 0,
+ initPathCount: (getLoadSettings().initialPaths || []).length,
};
this.activeContextQueue = new AsyncQueue();
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index 464d634425..075d80c070 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -4,7 +4,7 @@ import dedent from 'dedent-js';
import temp from 'temp';
import util from 'util';
-import Repository, {ABSENTLIKE, LOADINGLIKE} from '../../lib/models/repository';
+import Repository from '../../lib/models/repository';
import {expectedDelegates} from '../../lib/models/repository-states';
import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers';
@@ -45,16 +45,16 @@ describe('Repository', function() {
assert.isTrue(repository.isInState('Absent'));
});
- it('begins in an absentlike Undetermined state with .undetermined(ABSENTLIKE)', function() {
- repository = Repository.undetermined(ABSENTLIKE);
- assert.isTrue(repository.isInState('Undetermined'));
+ it('begins in an AbsentGuess state with .absentGuess()', function() {
+ repository = Repository.absentGuess();
+ assert.isTrue(repository.isInState('AbsentGuess'));
assert.isFalse(repository.showGitTabLoading());
assert.isTrue(repository.showGitTabInit());
});
- it('begins in a loadinglike Undetermined state with .undetermined(LOADINGLIKE)', function() {
- repository = Repository.undetermined(LOADINGLIKE);
- assert.isTrue(repository.isInState('Undetermined'));
+ it('begins in a LoadingGuess state with .guess()', function() {
+ repository = Repository.loadingGuess();
+ assert.isTrue(repository.isInState('LoadingGuess'));
assert.isTrue(repository.showGitTabLoading());
assert.isFalse(repository.showGitTabInit());
});
diff --git a/test/models/workdir-context.test.js b/test/models/workdir-context.test.js
index 088719ce1f..e92c718e23 100644
--- a/test/models/workdir-context.test.js
+++ b/test/models/workdir-context.test.js
@@ -2,7 +2,7 @@ import {CompositeDisposable, Disposable} from 'event-kit';
import {cloneRepository, sha} from '../helpers';
-import WorkdirContext, {absentWorkdirContext, ABSENTLIKE, LOADINGLIKE} from '../../lib/models/workdir-context';
+import WorkdirContext, {absentWorkdirContext} from '../../lib/models/workdir-context';
describe('WorkdirContext', function() {
let context, workingDirectory, subs;
@@ -128,14 +128,14 @@ describe('WorkdirContext', function() {
});
it('can be constructed containing an undetermined Repository that acts absent', function() {
- const undetermined = WorkdirContext.undetermined(ABSENTLIKE);
+ const undetermined = WorkdirContext.guess({});
assert.isTrue(undetermined.getRepository().isUndetermined());
assert.isFalse(undetermined.getRepository().showGitTabLoading());
assert.isTrue(undetermined.getRepository().showGitTabInit());
});
it("can be constructed containing an undetermined Repository that acts like it's loading", function() {
- const undetermined = WorkdirContext.undetermined(LOADINGLIKE);
+ const undetermined = WorkdirContext.guess({projectPathCount: 1});
assert.isTrue(undetermined.getRepository().isUndetermined());
assert.isTrue(undetermined.getRepository().showGitTabLoading());
assert.isFalse(undetermined.getRepository().showGitTabInit());
From ec61886c4fbac1b428880bdf4beedc44037103be Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 14:12:56 -0400
Subject: [PATCH 152/161] Document Repository states
---
lib/models/repository-states/absent-guess.js | 4 ++++
lib/models/repository-states/absent.js | 3 +++
lib/models/repository-states/cloning.js | 3 +++
lib/models/repository-states/destroyed.js | 3 +++
lib/models/repository-states/empty.js | 3 +++
lib/models/repository-states/initializing.js | 3 +++
lib/models/repository-states/loading-guess.js | 4 ++++
lib/models/repository-states/loading.js | 4 ++++
lib/models/repository-states/present.js | 5 +++++
9 files changed, 32 insertions(+)
diff --git a/lib/models/repository-states/absent-guess.js b/lib/models/repository-states/absent-guess.js
index c253ccc084..0f50b88082 100644
--- a/lib/models/repository-states/absent-guess.js
+++ b/lib/models/repository-states/absent-guess.js
@@ -1,5 +1,9 @@
import State from './state';
+/**
+ * Initial state to be used when we anticipate that the workspace will contain zero or many projects once bootstrapping
+ * has completed. Presents in the UI like the Absent state, but is "sticky" during the initial package activation.
+ */
export default class AbsentGuess extends State {
isAbsentGuess() {
return true;
diff --git a/lib/models/repository-states/absent.js b/lib/models/repository-states/absent.js
index 700dc27666..c282392e7a 100644
--- a/lib/models/repository-states/absent.js
+++ b/lib/models/repository-states/absent.js
@@ -1,5 +1,8 @@
import State from './state';
+/**
+ * No working directory is available in the workspace.
+ */
export default class Absent extends State {
isAbsent() {
return true;
diff --git a/lib/models/repository-states/cloning.js b/lib/models/repository-states/cloning.js
index a1eb34223b..8afe1c081a 100644
--- a/lib/models/repository-states/cloning.js
+++ b/lib/models/repository-states/cloning.js
@@ -2,6 +2,9 @@ import {mkdirs} from '../../helpers';
import State from './state';
+/**
+ * Git is asynchronously cloning a repository into this working directory.
+ */
export default class Cloning extends State {
constructor(repository, remoteUrl) {
super(repository);
diff --git a/lib/models/repository-states/destroyed.js b/lib/models/repository-states/destroyed.js
index 5144f35bba..0273989a9d 100644
--- a/lib/models/repository-states/destroyed.js
+++ b/lib/models/repository-states/destroyed.js
@@ -1,5 +1,8 @@
import State from './state';
+/**
+ * The package is being cleaned up or the context is being disposed some other way.
+ */
export default class Destroyed extends State {
start() {
this.didDestroy();
diff --git a/lib/models/repository-states/empty.js b/lib/models/repository-states/empty.js
index 720973a758..116ceae31c 100644
--- a/lib/models/repository-states/empty.js
+++ b/lib/models/repository-states/empty.js
@@ -1,5 +1,8 @@
import State from './state';
+/**
+ * The working directory exists, but contains no git repository yet.
+ */
export default class Empty extends State {
isEmpty() {
return true;
diff --git a/lib/models/repository-states/initializing.js b/lib/models/repository-states/initializing.js
index 5d3fa60621..385203d6ca 100644
--- a/lib/models/repository-states/initializing.js
+++ b/lib/models/repository-states/initializing.js
@@ -1,5 +1,8 @@
import State from './state';
+/**
+ * Git is asynchronously initializing a new repository in this working directory.
+ */
export default class Initializing extends State {
async start() {
await this.git().init(this.workdir());
diff --git a/lib/models/repository-states/loading-guess.js b/lib/models/repository-states/loading-guess.js
index 31cfdbc7a0..a323ff7c07 100644
--- a/lib/models/repository-states/loading-guess.js
+++ b/lib/models/repository-states/loading-guess.js
@@ -1,5 +1,9 @@
import State from './state';
+/**
+ * Initial state to be used when we anticipate that the workspace will contain a single project once bootstrapping
+ * has completed. Presents in the UI like the Loading state.
+ */
export default class LoadingGuess extends State {
isLoadingGuess() {
return true;
diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js
index dd9503faa5..a70420f557 100644
--- a/lib/models/repository-states/loading.js
+++ b/lib/models/repository-states/loading.js
@@ -1,5 +1,9 @@
import State from './state';
+/**
+ * Initial state to be used when it's uncertain whether or not a git repository is present in a working directory. If
+ * it is a git repository, transition to Present, otherwise transition to Empty.
+ */
export default class Loading extends State {
async start() {
if (await this.git().isGitRepository()) {
diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js
index c629f2e966..603a902af5 100644
--- a/lib/models/repository-states/present.js
+++ b/lib/models/repository-states/present.js
@@ -60,6 +60,11 @@ function invalidate() {
};
}
+/**
+ * State used when the working directory contains a valid git repository and can be interacted with. Performs
+ * actual git operations, caching the results, and broadcasts `onDidUpdate` events when write actions are
+ * performed.
+ */
export default class Present extends State {
constructor(repository, history) {
super(repository);
From 994804d25c6080945301dec048df43abfd5ca082 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 14:14:04 -0400
Subject: [PATCH 153/161] :fire: unused function
---
lib/models/repository-states/state.js | 4 ----
1 file changed, 4 deletions(-)
diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js
index 125e4dbd3e..95a8dd743c 100644
--- a/lib/models/repository-states/state.js
+++ b/lib/models/repository-states/state.js
@@ -471,10 +471,6 @@ export default class State {
}
}
-function unsupportedOperation(self, opName) {
- throw new Error(`${opName} is not available in ${self} state`);
-}
-
function unsupportedOperationPromise(self, opName) {
return Promise.reject(new Error(`${opName} is not available in ${self} state`));
}
From 81d8e405c727a2839c3c93606b0044a3e61d253c Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 14:14:15 -0400
Subject: [PATCH 154/161] Docs docs docs (without a k)
---
lib/models/repository.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 1c19776993..e3f8d340a7 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -180,8 +180,12 @@ export default class Repository {
}
// The methods named here will be delegated to the current State.
+//
// This list should match the methods decorated with @shouldDelegate in `lib/models/repository-states/state.js`. A test
// case in `test/models/repository.test.js` ensures that these sets match.
+//
+// Duplicated here rather than just using `expectedDelegates` directly so that this file is grep-friendly for answering
+// the question of "what all can a Repository do exactly".
const delegates = [
'isLoadingGuess',
'isAbsentGuess',
From f2e771f0063ca28e3dcb16f0c269cf828449d151 Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 14:36:17 -0400
Subject: [PATCH 155/161] State transition diagram
---
lib/models/repository-states/README.md | 11 +++++++++++
lib/models/repository-states/repo-states.svg | 2 ++
2 files changed, 13 insertions(+)
create mode 100644 lib/models/repository-states/README.md
create mode 100644 lib/models/repository-states/repo-states.svg
diff --git a/lib/models/repository-states/README.md b/lib/models/repository-states/README.md
new file mode 100644
index 0000000000..21629d9c74
--- /dev/null
+++ b/lib/models/repository-states/README.md
@@ -0,0 +1,11 @@
+This directory contains the various states that a `Repository` model can assume. Each state subclasses [`State`](./state), which provides each state with:
+
+* Trivial implementations of the state-dependent Repository functionality that return "empty" results where possible; Promises that resolve to empty arrays, null-object `Remote` or `Branch` implementations, `false`. This allows the UI components to freely query the current Repository for data regardless of whether an actual git repository exists or not.
+* Action and transition methods that throw exceptions or reject Promises.
+* UI state probe methods to allow individual states to customize the way that certain UI components respond while each state is active. For example, if `showGitTabLoading` is overridden to return `false`, the `.is-loading` class is added to the Git tab DOM node to allow it to be styled differently.
+
+Here's the current state transition diagram:
+
+
+
+Only the initial states (`Absent`, `AbsentGuess`, `Loading`, and `LoadingGuess`) are exported from the directory module.
diff --git a/lib/models/repository-states/repo-states.svg b/lib/models/repository-states/repo-states.svg
new file mode 100644
index 0000000000..5bbe1a4b82
--- /dev/null
+++ b/lib/models/repository-states/repo-states.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
From 311a7c8537e6caa255d6c4868cd11fbba4d6388b Mon Sep 17 00:00:00 2001
From: Ash Wilson
Date: Thu, 20 Apr 2017 14:43:18 -0400
Subject: [PATCH 156/161] Right, no SVGs in markdown renders
---
lib/models/repository-states/repo-states.png | Bin 0 -> 61057 bytes
lib/models/repository-states/repo-states.svg | 2 --
2 files changed, 2 deletions(-)
create mode 100644 lib/models/repository-states/repo-states.png
delete mode 100644 lib/models/repository-states/repo-states.svg
diff --git a/lib/models/repository-states/repo-states.png b/lib/models/repository-states/repo-states.png
new file mode 100644
index 0000000000000000000000000000000000000000..d31b310514cd4e8cf488d317dafabeaaa3dc16e0
GIT binary patch
literal 61057
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