From 88e52915e730ec70916cf3a23a478fe807570db5 Mon Sep 17 00:00:00 2001 From: Jaroslav Gorjatsev Date: Sat, 27 Feb 2016 08:31:01 +0200 Subject: [PATCH] support nested repos + specs, addressing atom/atom#2203 and atom/atom#1835 --- lib/directory-view.js | 12 ++ lib/directory.js | 41 +++-- lib/file.js | 16 +- lib/get-icon-services.js | 16 +- lib/helpers.coffee | 70 +++++++- lib/tree-view-package.js | 6 +- lib/tree-view.coffee | 12 +- menus/tree-view.cson | 3 + package.json | 12 ++ spec/tree-view-package-spec.coffee | 278 ++++++++++++++++++++++++++++- 10 files changed, 427 insertions(+), 39 deletions(-) diff --git a/lib/directory-view.js b/lib/directory-view.js index 44a798bb..8473873b 100644 --- a/lib/directory-view.js +++ b/lib/directory-view.js @@ -73,6 +73,7 @@ class DirectoryView { this.element.header = this.header this.element.entries = this.entries this.element.directoryName = this.directoryName + this.element.refreshRepoStatus = this.refreshRepoStatus.bind(this) } updateIcon () { @@ -134,6 +135,17 @@ class DirectoryView { } } + refreshRepoStatus (isRecursive = true, includeCollapsed = false) { + this.directory.refreshRepoStatus() + if (isRecursive) { + for (let entry of this.entries.children) { + if (entry.classList.contains('directory') && (entry.isExpanded || includeCollapsed)) { + entry.refreshRepoStatus(true, includeCollapsed) + } + } + } + } + toggleExpansion (isRecursive) { if (isRecursive == null) { isRecursive = false diff --git a/lib/directory.js b/lib/directory.js index 0fc96d38..52383bce 100644 --- a/lib/directory.js +++ b/lib/directory.js @@ -4,7 +4,7 @@ const {CompositeDisposable, Emitter} = require('atom') const fs = require('fs-plus') const PathWatcher = require('pathwatcher') const File = require('./file') -const {repoForPath} = require('./helpers') +const {repoForPath, getRepoCacheSize} = require('./helpers') module.exports = class Directory { @@ -30,6 +30,10 @@ class Directory { this.lowerCasePath = this.path.toLowerCase() this.lowerCaseRealPath = this.lowerCasePath } + this.repo = repoForPath(this.path) + if (this.repo && atom.config.get('tree-view.refreshVcsStatusOnProjectOpen') >= getRepoCacheSize()) { + this.refreshRepoStatus() + } if (this.isRoot == null) { this.isRoot = false @@ -69,8 +73,7 @@ class Directory { this.status = null this.entries = new Map() - const repo = repoForPath(this.path) - this.submodule = repo && repo.isSubmodule(this.path) + this.submodule = this.repo && this.repo.isSubmodule(this.path) this.subscribeToRepo() this.updateStatus() @@ -129,24 +132,30 @@ class Directory { } } + refreshRepoStatus () { + if (this.repo == null) return + + this.repo.refreshIndex() + this.repo.refreshStatus() + } + // Subscribe to project's repo for changes to the Git status of this directory. subscribeToRepo () { - const repo = repoForPath(this.path) - if (repo == null) return + if (this.repo == null) return - this.subscriptions.add(repo.onDidChangeStatus(event => { + this.subscriptions.add(this.repo.onDidChangeStatus(event => { if (this.contains(event.path)) { - this.updateStatus(repo) + this.updateStatus(this.repo) } })) - this.subscriptions.add(repo.onDidChangeStatuses(() => { - this.updateStatus(repo) + this.subscriptions.add(this.repo.onDidChangeStatuses(() => { + this.updateStatus(this.repo) })) } // Update the status property of this directory using the repo. - updateStatus () { - const repo = repoForPath(this.path) + updateStatus (repo = null) { + if (repo == null) repo = this.repo if (repo == null) return let newStatus = null @@ -156,7 +165,8 @@ class Directory { newStatus = 'ignored-name' } else { let status - if (this.isRoot) { + if (!repo.relativize(this.path + '/')) { + // repo root directory // repo.getDirectoryStatus will always fail for the // root because the path is relativized + concatenated with '/' // making the matching string be '/'. Then path.indexOf('/') @@ -184,8 +194,7 @@ class Directory { // Is the given path ignored? isPathIgnored (filePath) { if (atom.config.get('tree-view.hideVcsIgnoredFiles')) { - const repo = repoForPath(this.path) - if (repo && repo.isProjectAtRoot() && repo.isPathIgnored(filePath)) return true + if (this.repo && this.repo.isProjectAtRoot() && this.repo.isPathIgnored(filePath)) return true } if (atom.config.get('tree-view.hideIgnoredNames')) { @@ -276,7 +285,7 @@ class Directory { } catch (error) { names = [] } - names.sort(new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}).compare) + names.sort(new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare) const files = [] const directories = [] @@ -319,7 +328,7 @@ class Directory { // track the insertion index for the created views files.push(name) } else { - files.push(new File({name, fullPath, symlink, ignoredNames: this.ignoredNames, useSyncFS: this.useSyncFS, stats: statFlat})) + files.push(new File({ name, fullPath, symlink, ignoredNames: this.ignoredNames, useSyncFS: this.useSyncFS, stats: statFlat })) } } } diff --git a/lib/file.js b/lib/file.js index 8c26f2e8..0a4e57f1 100644 --- a/lib/file.js +++ b/lib/file.js @@ -15,6 +15,7 @@ class File { this.path = fullPath this.realPath = this.path + this.repo = repoForPath(this.path) this.subscribeToRepo() this.updateStatus() @@ -49,22 +50,21 @@ class File { // Subscribe to the project's repo for changes to the Git status of this file. subscribeToRepo () { - const repo = repoForPath(this.path) - if (repo == null) return + if (this.repo == null) return - this.subscriptions.add(repo.onDidChangeStatus(event => { + this.subscriptions.add(this.repo.onDidChangeStatus(event => { if (this.isPathEqual(event.path)) { - this.updateStatus(repo) + this.updateStatus(this.repo) } })) - this.subscriptions.add(repo.onDidChangeStatuses(() => { - this.updateStatus(repo) + this.subscriptions.add(this.repo.onDidChangeStatuses(() => { + this.updateStatus(this.repo) })) } // Update the status property of this directory using the repo. - updateStatus () { - const repo = repoForPath(this.path) + updateStatus (repo) { + if (repo == null) repo = this.repo if (repo == null) return let newStatus = null diff --git a/lib/get-icon-services.js b/lib/get-icon-services.js index ffde058b..5631a880 100644 --- a/lib/get-icon-services.js +++ b/lib/get-icon-services.js @@ -61,11 +61,19 @@ class IconServices { iconClass = 'icon-file-symlink-directory' } else { iconClass = 'icon-file-directory' - if (view.directory.isRoot) { - const repo = repoForPath(view.directory.path) - if (repo && repo.isProjectAtRoot()) iconClass = 'icon-repo' + let repo = repoForPath(view.directory.path) + if (repo) { + let relPath = repo.relativize(view.directory.path + '/') + if (relPath != null && relPath.length === 0) { + iconClass = 'icon-repo' + } } else { - if (view.directory.submodule) iconClass = 'icon-file-submodule' + if (view.directory.isRoot) { + const repo = repoForPath(view.directory.path) + if (repo && repo.isProjectAtRoot()) iconClass = 'icon-repo' + } else { + if (view.directory.submodule) iconClass = 'icon-file-submodule' + } } } classes.push(iconClass) diff --git a/lib/helpers.coffee b/lib/helpers.coffee index 8a93441c..b44e3965 100644 --- a/lib/helpers.coffee +++ b/lib/helpers.coffee @@ -1,11 +1,75 @@ path = require "path" +fs = require 'fs-plus' +{GitRepository} = require 'atom' module.exports = + repositoryCache: {} + fakeProjectRoots: [] + + getRepoCache: -> + module.exports.repositoryCache + + isFakeProjectRoot: (checkPath) -> + path.normalize(checkPath) in module.exports.fakeProjectRoots + + getRepoCacheSize: -> + Object.keys(module.exports.repositoryCache).length + + resetRepoCache: -> + module.exports.repositoryCache = {} + repoForPath: (goalPath) -> + result = null + project = null + projectIndex = null + _this = module.exports for projectPath, i in atom.project.getPaths() - if goalPath is projectPath or goalPath.indexOf(projectPath + path.sep) is 0 - return atom.project.getRepositories()[i] - null + if goalPath.indexOf(projectPath) is 0 + project = projectPath + projectIndex = i + # can't find related projects, so repo can't be assigned + return null unless project? + walkUpwards = (startDir, toDir, projectIndex) -> + if fs.existsSync(startDir + '/.git') + for provider in atom.project.repositoryProviders + if _this.repositoryCache[startDir] + return _this.repositoryCache[startDir] + for dProvider in atom.project.directoryProviders + break if directory = dProvider.directoryForURISync(startDir) + directory ?= atom.project.defaultDirectoryProvider.directoryForURISync(startDir) + repo = GitRepository.open(startDir, {project: provider.project, \ + refreshOnWindowFocus: atom.config.get('tree-view.refreshVcsStatusOnFocusChange') > _this.getRepoCacheSize()}) + return null unless repo + repo.onDidDestroy( -> + delete _this.repositoryCache[startDir] + indexToRemove = null + for dir, i in atom.project.getDirectories() + if startDir is dir.getPath() + indexToRemove = i + break + atom.project.rootDirectories.splice(indexToRemove, 1) + atom.project.repositories.splice(indexToRemove, 1) + ) + existsInAtom = false + for dir in atom.project.rootDirectories + if dir.getRealPathSync() is directory.getRealPathSync() + existsInAtom = true + break + if not existsInAtom + atom.project.repositories.splice(0, 0, repo) + atom.project.rootDirectories.splice(0, 0, directory) + _this.fakeProjectRoots.push(startDir) + _this.repositoryCache[startDir] = repo + return repo + if startDir is toDir + # top of project + if atom.project.getRepositories()[projectIndex] + return atom.project.getRepositories()[projectIndex] + return null + dirName = path.dirname(startDir) + return null if dirName is startDir # reached top + return walkUpwards(dirName, project, projectIndex) + return walkUpwards(path.normalize(goalPath), project, projectIndex) getStyleObject: (el) -> styleProperties = window.getComputedStyle(el) diff --git a/lib/tree-view-package.js b/lib/tree-view-package.js index f624e03d..9c1098df 100644 --- a/lib/tree-view-package.js +++ b/lib/tree-view-package.js @@ -1,5 +1,5 @@ const {Disposable, CompositeDisposable} = require('atom') - +const helpers = require('./helpers') const getIconServices = require('./get-icon-services') const TreeView = require('./tree-view') @@ -17,7 +17,8 @@ class TreeViewPackage { 'tree-view:duplicate': () => this.getTreeViewInstance().copySelectedEntry(), 'tree-view:remove': () => this.getTreeViewInstance().removeSelectedEntries(), 'tree-view:rename': () => this.getTreeViewInstance().moveSelectedEntry(), - 'tree-view:show-current-file-in-file-manager': () => this.getTreeViewInstance().showCurrentFileInFileManager() + 'tree-view:show-current-file-in-file-manager': () => this.getTreeViewInstance().showCurrentFileInFileManager(), + 'tree-view:refresh-vcs-status': () => this.getTreeViewInstance().refreshVcsStatus() })) const treeView = this.getTreeViewInstance() @@ -32,6 +33,7 @@ class TreeViewPackage { this.disposables.dispose() await this.treeViewOpenPromise // Wait for Tree View to finish opening before destroying it if (this.treeView) this.treeView.destroy() + helpers.resetRepoCache() this.treeView = null } diff --git a/lib/tree-view.coffee b/lib/tree-view.coffee index f848c60b..e7e03121 100644 --- a/lib/tree-view.coffee +++ b/lib/tree-view.coffee @@ -3,7 +3,7 @@ path = require 'path' _ = require 'underscore-plus' {BufferedProcess, CompositeDisposable, Emitter} = require 'atom' -{repoForPath, getStyleObject, getFullExtension} = require "./helpers" +{repoForPath, getStyleObject, getFullExtension, isFakeProjectRoot} = require "./helpers" fs = require 'fs-plus' AddDialog = require './add-dialog' @@ -237,6 +237,7 @@ class TreeView 'tree-view:toggle-vcs-ignored-files': -> toggleConfig 'tree-view.hideVcsIgnoredFiles' 'tree-view:toggle-ignored-names': -> toggleConfig 'tree-view.hideIgnoredNames' 'tree-view:remove-project-folder': (e) => @removeProjectFolder(e) + 'tree-view:refresh-folder-vcs-status': (e) => @refreshVcsStatus(e) [0..8].forEach (index) => atom.commands.add @element, "tree-view:open-selected-entry-in-pane-#{index + 1}", => @@ -258,6 +259,15 @@ class TreeView @disposables.add atom.config.onDidChange 'tree-view.squashDirectoryNames', => @updateRoots() + refreshVcsStatus: (e) -> + unless e + refreshFrom = @list.querySelectorAll('.project-root') + else + refreshFrom = [@selectedEntry()] + for refreshPoint in refreshFrom + if refreshPoint? and refreshPoint.refreshRepoStatus + refreshPoint.refreshRepoStatus(true, includeCollapsed = true) + toggle: -> atom.workspace.toggle(this) diff --git a/menus/tree-view.cson b/menus/tree-view.cson index f108645a..0f98a27b 100644 --- a/menus/tree-view.cson +++ b/menus/tree-view.cson @@ -36,6 +36,9 @@ {'label': 'Add Project Folder', 'command': 'application:add-project-folder'} {'type': 'separator'} + {'label': 'Refresh VCS Status', 'command': 'tree-view:refresh-folder-vcs-status'} + {'type': 'separator'} + {'label': 'Copy Full Path', 'command': 'tree-view:copy-full-path'} {'label': 'Copy Project Path', 'command': 'tree-view:copy-project-path'} {'label': 'Open in New Window', 'command': 'tree-view:open-in-new-window'} diff --git a/package.json b/package.json index 10f23517..d585b670 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,18 @@ "type": "boolean", "default": false, "description": "When opening a file, always focus an already-existing view of the file even if it's in a another pane." + }, + "refreshVcsStatusOnFocusChange": { + "title": "Refresh VCS Status On Focus Change for first N repos it met", + "type": "integer", + "default": 1, + "description": "Refresh VCS Status when focus of Atom editor changes of first N repos. In case of many nested repos Atom can be freezing, so consider this value to be low." + }, + "refreshVcsStatusOnProjectOpen": { + "title": "Refresh VCS Status On Project Open for first N repos it met", + "type": "integer", + "default": 10, + "description": "Refresh VCS Status once Atom project is opened of first N repos. Can decrease start-up time if amount of repositories and the option number are high." } } } diff --git a/spec/tree-view-package-spec.coffee b/spec/tree-view-package-spec.coffee index 1af6b5d5..2a84d729 100644 --- a/spec/tree-view-package-spec.coffee +++ b/spec/tree-view-package-spec.coffee @@ -6,6 +6,7 @@ os = require 'os' {remote, shell} = require 'electron' Directory = require '../lib/directory' eventHelpers = require "./event-helpers" +helpers = require '../lib/helpers' waitForPackageActivation = -> waitsForPromise -> @@ -3418,7 +3419,7 @@ describe "TreeView", -> newFile = path.join(newDir, 'new2') fs.writeFileSync(newFile, '') - atom.project.getRepositories()[0].getPathStatus(newFile) + helpers.getRepoCache()[projectPath].getPathStatus(newFile) ignoreFile = path.join(projectPath, '.gitignore') fs.writeFileSync(ignoreFile, 'ignored.txt') @@ -3428,7 +3429,7 @@ describe "TreeView", -> modifiedFile = path.join(projectPath, 'dir', 'b.txt') originalFileContent = fs.readFileSync(modifiedFile, 'utf8') fs.writeFileSync modifiedFile, 'ch ch changes' - atom.project.getRepositories()[0].getPathStatus(modifiedFile) + helpers.getRepoCache()[projectPath].getPathStatus(modifiedFile) treeView.useSyncFS = true treeView.updateRoots() @@ -3504,6 +3505,8 @@ describe "TreeView", -> expect(dirView.directory.updateStatus).toHaveBeenCalled() describe "on #darwin, when the project is a symbolic link to the repository root", -> + symlinkPath = null + beforeEach -> symlinkPath = temp.path('tree-view-project') fs.symlinkSync(projectPath, symlinkPath, 'junction') @@ -3511,7 +3514,7 @@ describe "TreeView", -> treeView.roots[0].entries.querySelectorAll('.directory')[1].expand() waitsFor (done) -> - disposable = atom.project.getRepositories()[0].onDidChangeStatuses -> + disposable = helpers.getRepoCache()[symlinkPath].onDidChangeStatuses -> disposable.dispose() done() @@ -3524,12 +3527,277 @@ describe "TreeView", -> describe "when a file loses its modified status", -> it "updates its and its parent directories' styles", -> fs.writeFileSync(modifiedFile, originalFileContent) - atom.project.getRepositories()[0].getPathStatus(modifiedFile) - + helpers.getRepoCache()[symlinkPath].getPathStatus(modifiedFile) expect(treeView.element.querySelector('.project-root .file.status-modified')).not.toExist() expect(treeView.element.querySelector('.project-root .directory.status-modified')).not.toExist() expect(treeView.element.querySelector('.project-root.status-modified')).not.toExist() + describe "Git status decorations for nested repositories", -> + repo1= null + subrepo1 = null + repo2 = null + + getDirectoryWithName = (treeView, name) -> + return Array.from(treeView.element.querySelectorAll('.tree-view-root .directory')).find((el) -> el.querySelector("div.header span[data-name='#{name}']")) + + getElementsContains = (element, selector, subSelector) -> + return Array.from(element.querySelectorAll(selector)).filter((el) -> el.querySelector(subSelector)) + + beforeEach -> + rootProjectPath = fs.realpathSync(temp.mkdirSync('tree-view-project')) + atom.project.setPaths([rootProjectPath]) + + prepareRepo = (subPath, repoName) -> + projectPath = path.normalize(rootProjectPath + '/' + subPath + repoName) + unless fs.existsSync(projectPath) + fs.mkdirSync(projectPath) + projectPath = fs.realpathSync(projectPath) + workingDirFixture = path.join(__dirname, 'fixtures', 'git', 'working-dir') + fs.copySync(workingDirFixture, projectPath) + fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + + newDir = path.join(projectPath, 'dir2') + fs.mkdirSync(newDir) + + newFile = path.join(newDir, 'new2') + fs.writeFileSync(newFile, '') + + pRepo = helpers.getRepoCache()[projectPath] + if pRepo + pRepo.getPathStatus(newFile) + + ignoreFile = path.join(projectPath, '.gitignore') + fs.writeFileSync(ignoreFile, 'ignored.txt') + ignoredFile = path.join(projectPath, 'ignored.txt') + fs.writeFileSync(ignoredFile, '') + + modifiedFile = path.join(projectPath, 'dir', 'b.txt') + originalFileContent = fs.readFileSync(modifiedFile, 'utf8') + fs.writeFileSync modifiedFile, 'ch ch changes' + if pRepo + pRepo.getPathStatus(modifiedFile) + + prepareRepo('', '') # root repo + prepareRepo('', 'repo1') + prepareRepo('repo1/', 'subrepo1') + prepareRepo('', 'repo2') + treeView.updateRoots() + + getDirectoryWithName(treeView, 'repo1').expand(true) + getDirectoryWithName(treeView, 'repo2').expand(true) + getDirectoryWithName(treeView, 'dir').expand() + getDirectoryWithName(treeView, 'dir2').expand() + repo1 = getDirectoryWithName(treeView, 'repo1') + repo2 = getDirectoryWithName(treeView, 'repo2') + subrepo1 = getDirectoryWithName(treeView, 'subrepo1') + waitsFor -> + treeView.element.querySelectorAll('.directory.status-modified').length is 8 + + describe "when the several git repositories nested under project root", -> + it "adds a custom style to root and all nested repos", -> + expect(treeView.element.querySelectorAll('.tree-view-root .icon-repo').length).toBe 4 + expect(getDirectoryWithName(treeView, 'repo1').querySelector('span')).toHaveClass 'icon-repo' + expect(getDirectoryWithName(treeView, 'subrepo1').querySelector('span')).toHaveClass 'icon-repo' + expect(getDirectoryWithName(treeView, 'repo2').querySelector('span')).toHaveClass 'icon-repo' + + describe "when a file is modified in either root or an any nested repo", -> + it "adds a custom style", -> + expect(treeView.element.querySelectorAll(".status-modified .file span[data-name='b.txt']").length).toBe 4 + expect(getElementsContains(repo1, '.file', "span[data-name='b.txt']")[0]).toHaveClass 'status-modified' + expect(getElementsContains(repo2, '.file', "span[data-name='b.txt']")[0]).toHaveClass 'status-modified' + expect(getElementsContains(subrepo1, '.file', "span[data-name='b.txt']")[0]).toHaveClass 'status-modified' + + + describe "when a directory is modified in either root or an any nested repo", -> + it "adds a custom style", -> + expect(treeView.element.querySelectorAll(".status-modified.directory>.header span[data-name='dir']").length).toBe 4 + expect(getElementsContains(repo1, '.directory', "span[data-name='dir']")[0]).toHaveClass 'status-modified' + expect(getElementsContains(repo2, '.directory', "span[data-name='dir']")[0]).toHaveClass 'status-modified' + expect(getElementsContains(subrepo1, '.directory', "span[data-name='dir']")[0]).toHaveClass 'status-modified' + + describe "when a file is new in either root or an any nested repo", -> + it "adds a custom style", -> + expect(treeView.element.querySelectorAll(".status-added.file span[data-name='new2']").length).toBe 4 + expect(getElementsContains(repo1, '.file', "span[data-name='new2']")[0]).toHaveClass 'status-added' + expect(getElementsContains(repo2, '.file', "span[data-name='new2']")[0]).toHaveClass 'status-added' + expect(getElementsContains(subrepo1, '.file', "span[data-name='new2']")[0]).toHaveClass 'status-added' + + describe "when a directory is new in either root or an any nested repo", -> + it "adds a custom style", -> + expect(treeView.element.querySelectorAll(".status-added.directory>.header span[data-name='dir2']").length).toBe 4 + expect(getElementsContains(repo1, '.directory', "span[data-name='dir2']")[0]).toHaveClass 'status-added' + expect(getElementsContains(repo2, '.directory', "span[data-name='dir2']")[0]).toHaveClass 'status-added' + expect(getElementsContains(subrepo1, '.directory', "span[data-name='dir2']")[0]).toHaveClass 'status-added' + + describe "when a file is ignored in either root or an any nested repo", -> + it "adds a custom style", -> + expect(treeView.element.querySelectorAll(".status-ignored.file span[data-name='ignored.txt']").length).toBe 4 + expect(getElementsContains(repo1, '.file', "span[data-name='ignored.txt']")[0]).toHaveClass 'status-ignored' + expect(getElementsContains(repo2, '.file', "span[data-name='ignored.txt']")[0]).toHaveClass 'status-ignored' + expect(getElementsContains(subrepo1, '.file', "span[data-name='ignored.txt']")[0]).toHaveClass 'status-ignored' + + describe "the refreshVcsStatusOnFocusChange config option set to 2", -> + rootProjectPath = null + rootPath = null + repo2Path = null + repo3Path = null + repo4Path = null + + beforeEach -> + atom.config.set("tree-view.refreshVcsStatusOnFocusChange", 2) + rootProjectPath = fs.realpathSync(temp.mkdirSync('tree-view-project')) + atom.project.setPaths([rootProjectPath]) + + prepareRepo = (subPath, repoName = null) -> + projectPath = path.normalize(rootProjectPath + '/' + subPath + repoName) + unless fs.existsSync(projectPath) + fs.mkdirSync(projectPath) + projectPath = fs.realpathSync(projectPath) + workingDirFixture = path.join(__dirname, 'fixtures', 'git', 'working-dir') + fs.copySync(workingDirFixture, projectPath) + fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + treeView.updateRoots() + projectPath + + rootPath = prepareRepo('', '') + repo2Path = prepareRepo('', 'repo2') + repo3Path = prepareRepo('', 'repo3') + repo4Path = prepareRepo('', 'repo4') + spyOn(helpers.getRepoCache()[rootPath], 'refreshStatus') + spyOn(helpers.getRepoCache()[repo2Path], 'refreshStatus') + spyOn(helpers.getRepoCache()[repo3Path], 'refreshStatus') + spyOn(helpers.getRepoCache()[repo4Path], 'refreshStatus') + # fire focus event + focused = false + onWindowFocus = -> + focused = true + window.addEventListener 'focus', onWindowFocus + ev = document.createEvent('HTMLEvents') + ev.initEvent('focus', true, true) + window.dispatchEvent(ev) + waitsFor -> + focused is true + + it "doesn't update status of 3rd or 4th repos", -> + # + expect(helpers.getRepoCache()[rootPath].refreshStatus.callCount).toBe 1 + expect(helpers.getRepoCache()[repo2Path].refreshStatus.callCount).toBe 1 + expect(helpers.getRepoCache()[repo3Path].refreshStatus.callCount).toBe 0 + expect(helpers.getRepoCache()[repo4Path].refreshStatus.callCount).toBe 0 + + describe "the refreshVcsStatusOnProjectOpen config option set to 2", -> + repo2 = null + repo3 = null + repo4 = null + projectRoot = null + + getElementsContains = (element, selector, subSelector) -> + return Array.from(element.querySelectorAll(selector)).filter((el) -> el.querySelector(subSelector)) + + beforeEach -> + atom.config.set("tree-view.refreshVcsStatusOnProjectOpen", 2) + rootProjectPath = fs.realpathSync(temp.mkdirSync('tree-view-project')) + treeView.useSyncFS = true + + prepareRepo = (subPath, repoName = null) -> + projectPath = path.normalize(rootProjectPath + '/' + subPath + repoName) + unless fs.existsSync(projectPath) + fs.mkdirSync(projectPath) + projectPath = fs.realpathSync(projectPath) + workingDirFixture = path.join(__dirname, 'fixtures', 'git', 'working-dir') + fs.copySync(workingDirFixture, projectPath) + fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + newDir = path.join(projectPath, 'dir2') + fs.mkdirSync(newDir) + newFile = path.join(newDir, 'new2') + fs.writeFileSync(newFile, '') + modifiedFile = path.join(projectPath, 'dir', 'b.txt') + originalFileContent = fs.readFileSync(modifiedFile, 'utf8') + fs.writeFileSync modifiedFile, 'ch ch changes' + + prepareRepo('', '') + prepareRepo('', 'repo2') + prepareRepo('', 'repo3') + prepareRepo('', 'repo4') + atom.project.setPaths([rootProjectPath]) + projectRoot = treeView.element.querySelector('.project-root') + repo2 = getElementsContains(projectRoot, ".directory", ".header span[data-name='repo2']")[0] + repo3 = getElementsContains(projectRoot, ".directory", ".header span[data-name='repo3']")[0] + repo4 = getElementsContains(projectRoot, ".directory", ".header span[data-name='repo4']")[0] + + it "doesn't update status of 3rd or 4th repos", -> + waitsFor -> + treeView.roots[0].entries.querySelectorAll('.directory.status-modified').length is 2 + runs -> + expect(Object.keys(helpers.getRepoCache()).length).toBe 4 + expect(repo2).toHaveClass 'status-modified' + expect(repo3).not.toHaveClass 'status-modified' + expect(repo4).not.toHaveClass 'status-modified' + + describe "the refresh-vcs-status command", -> + repo2 = null + repo3 = null + repo4 = null + projectRoot = null + + getElementsContains = (element, selector, subSelector) -> + return Array.from(element.querySelectorAll(selector)).filter((el) -> el.querySelector(subSelector)) + + beforeEach -> + atom.config.set("tree-view.refreshVcsStatusOnFocusChange", 0) + atom.config.set("tree-view.refreshVcsStatusOnProjectOpen", 0) + rootProjectPath = fs.realpathSync(temp.mkdirSync('tree-view-project')) + + prepareRepo = (subPath, repoName = null) -> + projectPath = path.normalize(rootProjectPath + '/' + subPath + repoName) + unless fs.existsSync(projectPath) + fs.mkdirSync(projectPath) + projectPath = fs.realpathSync(projectPath) + workingDirFixture = path.join(__dirname, 'fixtures', 'git', 'working-dir') + fs.copySync(workingDirFixture, projectPath) + fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + newDir = path.join(projectPath, 'dir2') + fs.mkdirSync(newDir) + newFile = path.join(newDir, 'new2') + fs.writeFileSync(newFile, '') + modifiedFile = path.join(projectPath, 'dir', 'b.txt') + originalFileContent = fs.readFileSync(modifiedFile, 'utf8') + fs.writeFileSync modifiedFile, 'ch ch changes' + + prepareRepo('', '') + prepareRepo('', 'repo2') + prepareRepo('', 'repo3') + prepareRepo('', 'repo4') + atom.project.setPaths([rootProjectPath]) + projectRoot = treeView.element.querySelector('.project-root') + repo2 = getElementsContains(projectRoot, ".directory", ".header span[data-name='repo2']")[0] + repo3 = getElementsContains(projectRoot, ".directory", ".header span[data-name='repo3']")[0] + repo4 = getElementsContains(projectRoot, ".directory", ".header span[data-name='repo4']")[0] + + it "does show option in menu", -> + expect(atom.contextMenu.templateForElement(repo2)).toContain({command: 'tree-view:refresh-folder-vcs-status', label: 'Refresh VCS Status'}) + + it "does update selected tree recursively", -> + expect(repo2).not.toHaveClass 'status-modified' + repo2.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 1})) + atom.commands.dispatch(repo2, 'tree-view:refresh-folder-vcs-status') + waitsFor -> + getElementsContains(projectRoot, ".directory.status-modified", ".header span[data-name='repo2']").length is 1 + runs -> + expect(repo2).toHaveClass 'status-modified' + expect(repo3).not.toHaveClass 'status-modified' + expect(repo4).not.toHaveClass 'status-modified' + + it "does update all tree if triggered from command palette", -> + expect(repo2).not.toHaveClass 'status-modified' + atom.commands.dispatch(treeView.element, 'tree-view:refresh-vcs-status') + waitsFor -> + treeView.element.querySelectorAll('.entries .directory.status-modified .icon-repo').length is 3 + runs -> + expect(repo2).toHaveClass 'status-modified' + expect(repo3).toHaveClass 'status-modified' + expect(repo4).toHaveClass 'status-modified' + describe "selecting items", -> [dirView, fileView1, fileView2, fileView3, fileView4, fileView5, treeView, rootDirPath, dirPath, filePath1, filePath2, filePath3, filePath4, filePath5] = []