const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') const path = require('path') const fs = require('fs-plus') const temp = require('temp').track() const GitRepository = require('../src/git-repository') const Project = require('../src/project') describe('GitRepository', () => { let repo beforeEach(() => { const gitPath = path.join(temp.dir, '.git') if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath) }) afterEach(() => { if (repo && !repo.isDestroyed()) repo.destroy() }) describe('@open(path)', () => { it('returns null when no repository is found', () => { expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() }) }) describe('new GitRepository(path)', () => { it('throws an exception when no repository is found', () => { expect(() => new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() }) }) describe('.getPath()', () => { it('returns the repository path for a .git directory path with a directory', () => { repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) it('returns the repository path for a repository path', () => { repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { it('returns true for an ignored path', () => { repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) expect(repo.isPathIgnored('a.txt')).toBeTruthy() }) it('returns false for a non-ignored path', () => { repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) expect(repo.isPathIgnored('b.txt')).toBeFalsy() }) }) describe('.isPathModified(path)', () => { let filePath, newPath beforeEach(() => { const workingDirPath = copyRepository() repo = new GitRepository(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') }) describe('when the path is unstaged', () => { it('returns false if the path has not been modified', () => { expect(repo.isPathModified(filePath)).toBeFalsy() }) it('returns true if the path is modified', () => { fs.writeFileSync(filePath, 'change') expect(repo.isPathModified(filePath)).toBeTruthy() }) it('returns true if the path is deleted', () => { fs.removeSync(filePath) expect(repo.isPathModified(filePath)).toBeTruthy() }) it('returns false if the path is new', () => { expect(repo.isPathModified(newPath)).toBeFalsy() }) }) }) describe('.isPathNew(path)', () => { let filePath, newPath beforeEach(() => { const workingDirPath = copyRepository() repo = new GitRepository(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") }) describe('when the path is unstaged', () => { it('returns true if the path is new', () => { expect(repo.isPathNew(newPath)).toBeTruthy() }) it("returns false if the path isn't new", () => { expect(repo.isPathNew(filePath)).toBeFalsy() }) }) }) describe('.checkoutHead(path)', () => { let filePath beforeEach(() => { const workingDirPath = copyRepository() repo = new GitRepository(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') }) it('no longer reports a path as modified after checkout', () => { expect(repo.isPathModified(filePath)).toBeFalsy() fs.writeFileSync(filePath, 'ch ch changes') expect(repo.isPathModified(filePath)).toBeTruthy() expect(repo.checkoutHead(filePath)).toBeTruthy() expect(repo.isPathModified(filePath)).toBeFalsy() }) it('restores the contents of the path to the original text', () => { fs.writeFileSync(filePath, 'ch ch changes') expect(repo.checkoutHead(filePath)).toBeTruthy() expect(fs.readFileSync(filePath, 'utf8')).toBe('') }) it('fires a status-changed event if the checkout completes successfully', () => { fs.writeFileSync(filePath, 'ch ch changes') repo.getPathStatus(filePath) const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) repo.checkoutHead(filePath) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) repo.checkoutHead(filePath) expect(statusHandler.callCount).toBe(1) }) }) describe('.checkoutHeadForEditor(editor)', () => { let filePath, editor beforeEach(async () => { spyOn(atom, 'confirm') const workingDirPath = copyRepository() repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') editor = await atom.workspace.open(filePath) }) it('displays a confirmation dialog by default', () => { // Permissions issues with this test on Windows if (process.platform === 'win32') return atom.confirm.andCallFake(({buttons}) => buttons.OK()) atom.config.set('editor.confirmCheckoutHeadRevision', true) repo.checkoutHeadForEditor(editor) expect(fs.readFileSync(filePath, 'utf8')).toBe('') }) it('does not display a dialog when confirmation is disabled', () => { // Flakey EPERM opening a.txt on Win32 if (process.platform === 'win32') return atom.config.set('editor.confirmCheckoutHeadRevision', false) repo.checkoutHeadForEditor(editor) expect(fs.readFileSync(filePath, 'utf8')).toBe('') expect(atom.confirm).not.toHaveBeenCalled() }) }) describe('.destroy()', () => { it('throws an exception when any method is called after it is called', () => { repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) repo.destroy() expect(() => repo.getShortHead()).toThrow() }) }) describe('.getPathStatus(path)', () => { let filePath beforeEach(() => { const workingDirectory = copyRepository() repo = new GitRepository(workingDirectory) filePath = path.join(workingDirectory, 'file.txt') }) it('trigger a status-changed event when the new status differs from the last cached one', () => { const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') let status = repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) fs.writeFileSync(filePath, 'abc') status = repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe(1) }) }) describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath beforeEach(() => { const workingDirectory = copyRepository() repo = new GitRepository(workingDirectory) directoryPath = path.join(workingDirectory, 'dir') filePath = path.join(directoryPath, 'b.txt') }) it('gets the status based on the files inside the directory', () => { expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(false) fs.writeFileSync(filePath, 'abc') repo.getPathStatus(filePath) expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(true) }) }) describe('.refreshStatus()', () => { let newPath, modifiedPath, cleanPath, workingDirectory beforeEach(() => { workingDirectory = copyRepository() repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config}) modifiedPath = path.join(workingDirectory, 'file.txt') newPath = path.join(workingDirectory, 'untracked.txt') cleanPath = path.join(workingDirectory, 'other.txt') fs.writeFileSync(cleanPath, 'Full of text') fs.writeFileSync(newPath, '') newPath = fs.absolute(newPath) }) it('returns status information for all new and modified files', async () => { const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatuses(statusHandler) fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() expect(statusHandler.callCount).toBe(1) expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() expect(repo.isStatusNew(repo.getCachedPathStatus(newPath) )).toBeTruthy() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) it('caches the proper statuses when a subdir is open', async () => { const subDir = path.join(workingDirectory, 'dir') fs.mkdirSync(subDir) const filePath = path.join(subDir, 'b.txt') fs.writeFileSync(filePath, '') atom.project.setPaths([subDir]) await atom.workspace.open('b.txt') repo = atom.project.getRepositories()[0] await repo.refreshStatus() const status = repo.getCachedPathStatus(filePath) expect(repo.isStatusModified(status)).toBe(false) expect(repo.isStatusNew(status)).toBe(false) }) it('works correctly when the project has multiple folders (regression)', async () => { atom.project.addPath(workingDirectory) atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) await repo.refreshStatus() expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) it('caches statuses that were looked up synchronously', async () => { const originalContent = 'undefined' fs.writeFileSync(modifiedPath, 'making this path modified') repo.getPathStatus('file.txt') fs.writeFileSync(modifiedPath, originalContent) await repo.refreshStatus() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() }) }) describe('buffer events', () => { let editor beforeEach(async () => { atom.project.setPaths([copyRepository()]) const refreshPromise = new Promise(resolve => atom.project.getRepositories()[0].onDidChangeStatuses(resolve)) editor = await atom.workspace.open('other.txt') await refreshPromise }) it('emits a status-changed event when a buffer is saved', async () => { editor.insertNewline() const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) await editor.save() expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) }) it('emits a status-changed event when a buffer is reloaded', async () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) await editor.getBuffer().reload() expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) await editor.getBuffer().reload() expect(statusHandler.callCount).toBe(1) }) it("emits a status-changed event when a buffer's path changes", () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) editor.getBuffer().emitter.emit('did-change-path') expect(statusHandler.callCount).toBe(1) }) it('stops listening to the buffer when the repository is destroyed (regression)', () => { atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() }) }) describe('when a project is deserialized', () => { let buffer, project2, statusHandler afterEach(() => { if (project2) project2.destroy() }) it('subscribes to all the serialized buffers in the project', async () => { atom.project.setPaths([copyRepository()]) await atom.workspace.open('file.txt') project2 = new Project({ notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, grammarRegistry: atom.grammars, applicationDelegate: atom.applicationDelegate }) await project2.deserialize(atom.project.serialize({isUnloading: false})) buffer = project2.getBuffers()[0] const originalContent = buffer.getText() buffer.append('changes') statusHandler = jasmine.createSpy('statusHandler') project2.getRepositories()[0].onDidChangeStatus(statusHandler) await buffer.save() expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) }) }) }) function copyRepository () { const workingDirPath = temp.mkdirSync('atom-spec-git') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) return workingDirPath }