diff --git a/lib/models/repository.js b/lib/models/repository.js index ee84a3a51c..0fe364781f 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -31,6 +31,146 @@ export class CommitError extends Error { } } +class BaseRepository { + static from(repository) { + return new this( + repository.workingDirectoryPath, + repository.git, + repository.emitter, + repository.destroyed, + ); + } + + constructor(workingDirectoryPath, gitStrategy, emitter, destroyed) { + this.workingDirectoryPath = workingDirectoryPath; + this.git = gitStrategy; + this.emitter = emitter || new Emitter(); + this.destroyed = destroyed; + } + + isLoading() { + return false; + } + + isOpen() { + return false; + } + + isAbsent() { + return false; + } + + isDestroyed() { + return this.destroyed; + } + + destroy() { + if (this.isDestroyed()) { + return; + } + + this.destroyed = true; + this.emitter.emit('did-destroy'); + this.emitter.dispose(); + this.windowFocusDisposable.dispose(); + } + + 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() { + // Do nothing. + } +} + +class LoadingRepository extends BaseRepository { + isLoading() { + return true; + } + + async open() { + if (await this.git.isGitRepository()) { + const openRepo = OpenRepository.from(this); + await openRepo.updateDiscardHistory(); + return openRepo; + } else { + return AbsentRepository.from(this); + } + } +} + +class OpenRepository extends BaseRepository { + constructor(workingDirectoryPath, gitStrategy, emitter, destroyed) { + super(workingDirectoryPath, gitStrategy, emitter, destroyed); + + this.stagedFilePatchesByPath = new Map(); + this.stagedFilePatchesSinceParentCommitByPath = new Map(); + this.unstagedFilePatchesByPath = new Map(); + + this.discardHistory = new DiscardHistory( + this.createBlob, + this.expandBlobToFile, + this.mergeFile, + {maxHistoryLength: 60}, + ); + + const onWindowFocus = () => this.updateDiscardHistory(); + window.addEventListener('focus', onWindowFocus); + this.windowFocusDisposable = new Disposable(() => window.removeEventListener('focus', onWindowFocus)); + } + + isOpen() { + return true; + } + + destroy() { + super(); + + this.windowFocusDisposable.dispose(); + } + + refresh() { + this.clearCachedFilePatches(); + this.fileStatusesPromise = null; + this.stagedChangesSinceParentCommitPromise = null; + this.didUpdate(); + } + + fetchStatusesForChangedFiles() { + return this.git.getStatusesForChangedFiles(); + } + + getStatusesForChangedFiles() { + if (!this.fileStatusesPromise) { + this.fileStatusesPromise = this.fetchStatusesForChangedFiles(); + } + return this.fileStatusesPromise; + } +} + +class AbsentRepository extends BaseRepository { + isAbsent() { + return true; + } +} + export default class Repository { static async open(workingDirectoryPath, gitStrategy) { const strategy = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath); @@ -451,6 +591,7 @@ export default class Repository { await this.setConfig('atomGithub.historySha', historySha); } + @autobind async updateDiscardHistory() { const historySha = await this.git.getConfig('atomGithub.historySha'); const history = historySha ? JSON.parse(await this.git.getBlobContents(historySha)) : {}; diff --git a/test/models/repository.test.js b/test/models/repository.test.js index c10988f27a..260751b77c 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -40,10 +40,42 @@ describe('Repository', function() { }); }); + describe.only('repository state', function() { + it('transitions from loading to open on a workdir with a git repository', async function() { + const workdirPath = await cloneRepository('three-files'); + + const loading = Repository.forPath(workdirPath); + assert.isTrue(loading.isLoading()); + assert.isFalse(loading.isOpen()); + assert.isFalse(loading.isAbsent()); + + const loaded = await loading.open(); + assert.isFalse(loaded.isLoading()); + assert.isTrue(loaded.isOpen()); + assert.isFalse(loaded.isAbsent()); + }); + + it('transitions from loading to absent on a workdir without a git repository', async function() { + const repositorylessPath = fs.realpathSync(temp.mkdirSync()); + + const loading = Repository.forPath(repositorylessPath); + assert.isTrue(loading.isLoading()); + assert.isFalse(loading.isOpen()); + assert.isFalse(loading.isAbsent()); + + const absent = await loading.open(); + assert.isFalse(absent.isLoading()); + assert.isFalse(absent.isOpen()); + assert.isTrue(absent.isAbsent()); + }); + }); + 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); + const nullRepo = await Repository.open(soonToBeRepositoryPath); + + const repo = nullRepo.init(soonToBeRepositoryPath); assert.isTrue(await repo.isGitRepository()); assert.equal(repo.getWorkingDirectoryPath(), soonToBeRepositoryPath); });