From 51a035e31d7ae81ec46946e49be2ca7ee13f4933 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 15:54:11 +0200 Subject: [PATCH 001/254] Add nodegit to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b7d079123..093ce9e6f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "key-path-helpers": "^0.3.0", "less-cache": "0.22", "marked": "^0.3.4", + "nodegit": "~0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 8e744502fa0fcf427612f26eeb0bc954732db960 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 15:54:42 +0200 Subject: [PATCH 002/254] start spiking out async repo --- spec/git-repository-async-spec.coffee | 303 ++++++++++++++++++++++++++ src/git-repository-async.js | 23 ++ 2 files changed, 326 insertions(+) create mode 100644 spec/git-repository-async-spec.coffee create mode 100644 src/git-repository-async.js diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee new file mode 100644 index 000000000..4a881ee23 --- /dev/null +++ b/spec/git-repository-async-spec.coffee @@ -0,0 +1,303 @@ +temp = require 'temp' +GitRepositoryAsync = require '../src/git-repository-async' +fs = require 'fs-plus' +path = require 'path' +Task = require '../src/task' +Project = require '../src/project' + +copyRepository = -> + workingDirPath = temp.mkdirSync('atom-working-dir') + fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) + fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) + workingDirPath + +fdescribe "GitRepositoryAsync", -> + repo = null + + # beforeEach -> + # gitPath = path.join(temp.dir, '.git') + # fs.removeSync(gitPath) if fs.isDirectorySync(gitPath) + # + # afterEach -> + # repo.destroy() if repo?.repo? + + describe "@open(path)", -> + + # This just exercises the framework, but I'm trying to match the sync specs to start + it "repo is null when no repository is found", -> + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + + waitsFor -> + repo._opening is false + + runs -> + expect(repo.repo).toBe null + + describe ".getPath()", -> + it "returns the repository path for a .git directory path", -> + repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) + + waitsForPromise -> + repo.getPath + + runs -> + 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') + + xdescribe ".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() + + xdescribe ".isPathModified(path)", -> + [repo, filePath, newPath] = [] + + beforeEach -> + workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + + xdescribe "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() + + xdescribe ".isPathNew(path)", -> + [filePath, newPath] = [] + + beforeEach -> + 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") + + xdescribe "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() + + xdescribe ".checkoutHead(path)", -> + [filePath] = [] + + beforeEach -> + 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) + 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 + + xdescribe ".checkoutHeadForEditor(editor)", -> + [filePath, editor] = [] + + beforeEach -> + workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise -> + atom.workspace.open(filePath) + + runs -> + editor = atom.workspace.getActiveTextEditor() + + it "displays a confirmation dialog by default", -> + spyOn(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", -> + spyOn(atom, 'confirm') + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe '' + expect(atom.confirm).not.toHaveBeenCalled() + + xdescribe ".destroy()", -> + it "throws an exception when any method is called after it is called", -> + repo = new GitRepository(require.resolve('./fixtures/git/master.git/HEAD')) + repo.destroy() + expect(-> repo.getShortHead()).toThrow() + + xdescribe ".getPathStatus(path)", -> + [filePath] = [] + + beforeEach -> + 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", -> + statusHandler = jasmine.createSpy("statusHandler") + repo.onDidChangeStatus statusHandler + fs.writeFileSync(filePath, '') + 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 + + xdescribe ".getDirectoryStatus(path)", -> + [directoryPath, filePath] = [] + + beforeEach -> + 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 + + xdescribe ".refreshStatus()", -> + [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] + + beforeEach -> + workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + 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 # specs could be running under symbol path. + + it "returns status information for all new and modified files", -> + fs.writeFileSync(modifiedPath, 'making this path modified') + statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses statusHandler + repo.refreshStatus() + + waitsFor -> + statusHandler.callCount > 0 + + runs -> + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + + xdescribe "buffer events", -> + [editor] = [] + + beforeEach -> + atom.project.setPaths([copyRepository()]) + + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> editor = o + + it "emits a status-changed event when a buffer is saved", -> + editor.insertNewline() + + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus statusHandler + 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", -> + fs.writeFileSync(editor.getPath(), 'changed') + + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus statusHandler + editor.getBuffer().reload() + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + 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') + + 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() + + xdescribe "when a project is deserialized", -> + [buffer, project2] = [] + + afterEach -> + project2?.destroy() + + it "subscribes to all the serialized buffers in the project", -> + atom.project.setPaths([copyRepository()]) + + waitsForPromise -> + atom.workspace.open('file.txt') + + runs -> + project2 = Project.deserialize(atom.project.serialize()) + buffer = project2.getBuffers()[0] + + waitsFor -> + buffer.loaded + + runs -> + originalContent = buffer.getText() + buffer.append('changes') + + statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].onDidChangeStatus statusHandler + buffer.save() + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} diff --git a/src/git-repository-async.js b/src/git-repository-async.js new file mode 100644 index 000000000..66d51743f --- /dev/null +++ b/src/git-repository-async.js @@ -0,0 +1,23 @@ +"use babel"; + +const Git = require('nodegit') + +module.exports = class GitRepositoryAsync { + static open(path) { + // QUESTION: Should this wrap Git.Repository and reject with a nicer message? + return new GitRepositoryAsync(Git.Repository.open(path)) + } + + constructor (openPromise) { + this.repo = null + // this could be replaced with a function + this._opening = true + + openPromise.then( (repo) => { + this.repo = repo + this._opening = false + }).catch( (e) => { + this._opening = false + }) + } +} From b566e47a08bab8ddbb3632b28ebd34239d12d3f9 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 16:51:39 +0200 Subject: [PATCH 003/254] getPath --- spec/git-repository-async-spec.coffee | 27 ++++++++++++++++++++------- src/git-repository-async.js | 9 +++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 4a881ee23..de752084d 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -34,18 +34,31 @@ fdescribe "GitRepositoryAsync", -> expect(repo.repo).toBe null describe ".getPath()", -> - it "returns the repository path for a .git directory path", -> + # XXX HEAD isn't a git directory.. what's this spec supposed to be about? + xit "returns the repository path for a .git directory path", -> + # Rejects as malformed repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) - waitsForPromise -> - repo.getPath + onSuccess = jasmine.createSpy('onSuccess') - runs -> - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + waitsForPromise -> + repo.getPath().then(onSuccess) + + runs -> + expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) 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') + repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git')) + + onSuccess = jasmine.createSpy('onSuccess') + + waitsForPromise -> + repo.getPath().then(onSuccess) + + runs -> + expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) xdescribe ".isPathIgnored(path)", -> it "returns true for an ignored path", -> diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 66d51743f..d36664e09 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -13,11 +13,20 @@ module.exports = class GitRepositoryAsync { // this could be replaced with a function this._opening = true + // Do I use this outside of tests? openPromise.then( (repo) => { this.repo = repo this._opening = false }).catch( (e) => { this._opening = false }) + + this.repoPromise = openPromise + } + + getPath () { + return this.repoPromise.then( (repo) => { + return Promise.resolve(repo.path().replace(/\/$/, '')) + }) } } From bc3e8f02adde26f57844d7b35a34cf34e6fc37dc Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 17:00:47 +0200 Subject: [PATCH 004/254] isPathIgnored --- spec/git-repository-async-spec.coffee | 29 +++++++++++++++++++-------- src/git-repository-async.js | 6 ++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index de752084d..b80b2940f 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -11,6 +11,9 @@ copyRepository = -> fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) workingDirPath +openFixture = (fixture)-> + GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) + fdescribe "GitRepositoryAsync", -> repo = null @@ -49,7 +52,7 @@ fdescribe "GitRepositoryAsync", -> expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) it "returns the repository path for a repository path", -> - repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git')) + repo = openFixture('master.git') onSuccess = jasmine.createSpy('onSuccess') @@ -60,14 +63,24 @@ fdescribe "GitRepositoryAsync", -> expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) - xdescribe ".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() + describe ".isPathIgnored(path)", -> + it "resolves true for an ignored path", -> + repo = openFixture('ignore.git') + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathIgnored('a.txt').then(onSuccess).catch (e) -> console.log e + + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() + + it "resolves false for a non-ignored path", -> + repo = openFixture('ignore.git') + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathIgnored('b.txt').then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - it "returns false for a non-ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('b.txt')).toBeFalsy() xdescribe ".isPathModified(path)", -> [repo, filePath, newPath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d36664e09..e6d5a8f5d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -29,4 +29,10 @@ module.exports = class GitRepositoryAsync { return Promise.resolve(repo.path().replace(/\/$/, '')) }) } + + isPathIgnored(_path) { + return this.repoPromise.then( (repo) => { + return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) + }) + } } From 62310e6f41b65bc3b91be05f3bab4f350527d63f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 18:10:32 +0200 Subject: [PATCH 005/254] isPathModified --- spec/git-repository-async-spec.coffee | 56 ++++++++++++++++++++------- src/git-repository-async.js | 16 ++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index b80b2940f..180988d83 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -1,15 +1,19 @@ temp = require 'temp' GitRepositoryAsync = require '../src/git-repository-async' fs = require 'fs-plus' +os = require 'os' path = require 'path' Task = require '../src/task' Project = require '../src/project' +# Clean up when the process exits +temp.track() + copyRepository = -> workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - workingDirPath + fs.realpathSync(workingDirPath) openFixture = (fixture)-> GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) @@ -82,29 +86,55 @@ fdescribe "GitRepositoryAsync", -> expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - xdescribe ".isPathModified(path)", -> - [repo, filePath, newPath] = [] + describe ".isPathModified(path)", -> + [repo, filePath, newPath, emptyPath] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = new GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + emptyPath = path.join(workingDirPath, 'empty-path.txt') - xdescribe "when the path is unstaged", -> - it "returns false if the path has not been modified", -> - expect(repo.isPathModified(filePath)).toBeFalsy() + describe "when the path is unstaged", -> + it "resolves false if the path has not been modified", -> + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - it "returns true if the path is modified", -> + it "resolves true if the path is modified", -> fs.writeFileSync(filePath, "change") - expect(repo.isPathModified(filePath)).toBeTruthy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - it "returns true if the path is deleted", -> + + it "resolves true if the path is deleted", -> fs.removeSync(filePath) - expect(repo.isPathModified(filePath)).toBeTruthy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - it "returns false if the path is new", -> - expect(repo.isPathModified(newPath)).toBeFalsy() + it "resolves false if the path is new", -> + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(newPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + + it "resolves false if the path is invalid", -> + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(emptyPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() xdescribe ".isPathNew(path)", -> [filePath, newPath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e6d5a8f5d..445e88ccb 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,6 +1,7 @@ "use babel"; const Git = require('nodegit') +const path = require('path') module.exports = class GitRepositoryAsync { static open(path) { @@ -35,4 +36,19 @@ module.exports = class GitRepositoryAsync { return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) }) } + + isPathModified(_path) { + // Surely I'm missing a built-in way to do this + var basePath = null + return this.repoPromise.then( (repo) => { + basePath = repo.workdir() + return repo.getStatus() + }).then( (statuses) => { + console.log(statuses.map(function(x){return x.path()})); + ret = statuses.filter((status)=> { + return _path == path.join(basePath, status.path()) && (status.isModified() || status.isDeleted()) + }).length > 0 + return Promise.resolve(ret) + }) + } } From 7679ff93d33aa42cb5e1fa56329fd220a672e9ba Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 18:26:20 +0200 Subject: [PATCH 006/254] isPathNew --- spec/git-repository-async-spec.coffee | 21 +++++++++++++++------ src/git-repository-async.js | 22 +++++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 180988d83..0eba6a4f4 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -91,7 +91,7 @@ fdescribe "GitRepositoryAsync", -> beforeEach -> workingDirPath = copyRepository() - repo = new GitRepositoryAsync.open(workingDirPath) + repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") @@ -136,22 +136,31 @@ fdescribe "GitRepositoryAsync", -> runs -> expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - xdescribe ".isPathNew(path)", -> + describe ".isPathNew(path)", -> [filePath, newPath] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") - xdescribe "when the path is unstaged", -> + describe "when the path is unstaged", -> it "returns true if the path is new", -> - expect(repo.isPathNew(newPath)).toBeTruthy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathNew(newPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() it "returns false if the path isn't new", -> - expect(repo.isPathNew(filePath)).toBeFalsy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(newPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + xdescribe ".checkoutHead(path)", -> [filePath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 445e88ccb..6a07c93b3 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -37,16 +37,32 @@ module.exports = class GitRepositoryAsync { }) } - isPathModified(_path) { + _filterStatusesByPath(_path) { // Surely I'm missing a built-in way to do this var basePath = null return this.repoPromise.then( (repo) => { basePath = repo.workdir() return repo.getStatus() }).then( (statuses) => { - console.log(statuses.map(function(x){return x.path()})); + return statuses.filter(function (status) { + return _path == path.join(basePath, status.path()) + }) + }) + } + + isPathModified(_path) { + return this._filterStatusesByPath(_path).then(function(statuses) { ret = statuses.filter((status)=> { - return _path == path.join(basePath, status.path()) && (status.isModified() || status.isDeleted()) + return status.isModified() || status.isDeleted() + }).length > 0 + return Promise.resolve(ret) + }) + } + + isPathNew(_path) { + return this._filterStatusesByPath(_path).then(function(statuses) { + ret = statuses.filter((status)=> { + return status.isNew() }).length > 0 return Promise.resolve(ret) }) From ca4ac209d6c477da21971112d451e3ac74b5e32b Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 18:49:14 +0200 Subject: [PATCH 007/254] Spike checkoutHead --- src/git-repository-async.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 6a07c93b3..59df07a0b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -52,8 +52,8 @@ module.exports = class GitRepositoryAsync { isPathModified(_path) { return this._filterStatusesByPath(_path).then(function(statuses) { - ret = statuses.filter((status)=> { - return status.isModified() || status.isDeleted() + var ret = statuses.filter((status)=> { + return status.isModified() }).length > 0 return Promise.resolve(ret) }) @@ -61,10 +61,19 @@ module.exports = class GitRepositoryAsync { isPathNew(_path) { return this._filterStatusesByPath(_path).then(function(statuses) { - ret = statuses.filter((status)=> { + var ret = statuses.filter((status)=> { return status.isNew() }).length > 0 return Promise.resolve(ret) }) } + + checkoutHead (_path) { + return this.repoPromise.then(function (repo) { + var checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [_path] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + Git.Checkout.head(repo, checkoutOptions) + }) + } } From 3b15f4b2598ffc7f67d2ecfff05b927e2fc629d5 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 18:50:43 +0200 Subject: [PATCH 008/254] Conform to standardjs style --- src/git-repository-async.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 59df07a0b..aec474f45 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,10 +1,10 @@ -"use babel"; +'use babel' const Git = require('nodegit') const path = require('path') module.exports = class GitRepositoryAsync { - static open(path) { + static open (path) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? return new GitRepositoryAsync(Git.Repository.open(path)) } @@ -15,10 +15,10 @@ module.exports = class GitRepositoryAsync { this._opening = true // Do I use this outside of tests? - openPromise.then( (repo) => { + openPromise.then((repo) => { this.repo = repo this._opening = false - }).catch( (e) => { + }).catch((e) => { this._opening = false }) @@ -26,42 +26,42 @@ module.exports = class GitRepositoryAsync { } getPath () { - return this.repoPromise.then( (repo) => { + return this.repoPromise.then((repo) => { return Promise.resolve(repo.path().replace(/\/$/, '')) }) } - isPathIgnored(_path) { - return this.repoPromise.then( (repo) => { + isPathIgnored (_path) { + return this.repoPromise.then((repo) => { return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) }) } - _filterStatusesByPath(_path) { + _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this var basePath = null - return this.repoPromise.then( (repo) => { + return this.repoPromise.then((repo) => { basePath = repo.workdir() return repo.getStatus() - }).then( (statuses) => { + }).then((statuses) => { return statuses.filter(function (status) { - return _path == path.join(basePath, status.path()) + return _path === path.join(basePath, status.path()) }) }) } - isPathModified(_path) { - return this._filterStatusesByPath(_path).then(function(statuses) { - var ret = statuses.filter((status)=> { + isPathModified (_path) { + return this._filterStatusesByPath(_path).then(function (statuses) { + var ret = statuses.filter((status) => { return status.isModified() }).length > 0 return Promise.resolve(ret) }) } - isPathNew(_path) { - return this._filterStatusesByPath(_path).then(function(statuses) { - var ret = statuses.filter((status)=> { + isPathNew (_path) { + return this._filterStatusesByPath(_path).then(function (statuses) { + var ret = statuses.filter((status) => { return status.isNew() }).length > 0 return Promise.resolve(ret) From dfb24ce617d9178cb79bffbdf9c55a7c91cd5799 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 19:12:08 +0200 Subject: [PATCH 009/254] Clean up init a bit --- spec/git-repository-async-spec.coffee | 4 ++-- src/git-repository-async.js | 22 ++++++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 0eba6a4f4..068756a72 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -34,8 +34,8 @@ fdescribe "GitRepositoryAsync", -> it "repo is null when no repository is found", -> repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - waitsFor -> - repo._opening is false + waitsForPromise {shouldReject: true}, -> + repo.repoPromise runs -> expect(repo.repo).toBe null diff --git a/src/git-repository-async.js b/src/git-repository-async.js index aec474f45..f3916eb19 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,26 +3,20 @@ const Git = require('nodegit') const path = require('path') +// GitUtils is temporarily used for ::relativize only, because I don't want +// to port it just yet. TODO: remove +const GitUtils = require('git-utils') + module.exports = class GitRepositoryAsync { static open (path) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? - return new GitRepositoryAsync(Git.Repository.open(path)) + return new GitRepositoryAsync(path) } - constructor (openPromise) { + constructor (path) { this.repo = null - // this could be replaced with a function - this._opening = true - - // Do I use this outside of tests? - openPromise.then((repo) => { - this.repo = repo - this._opening = false - }).catch((e) => { - this._opening = false - }) - - this.repoPromise = openPromise + this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize + this.repoPromise = Git.Repository.open(path) } getPath () { From df949d3a3dcf3f446380d3ac437dc2d5173c360f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 19:24:34 +0200 Subject: [PATCH 010/254] more ::checkoutHead specs --- spec/git-repository-async-spec.coffee | 49 ++++++++++++++++----------- src/git-repository-async.js | 4 +-- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 068756a72..7de2a0b01 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -113,15 +113,6 @@ fdescribe "GitRepositoryAsync", -> runs -> expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "resolves true if the path is deleted", -> - fs.removeSync(filePath) - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - it "resolves false if the path is new", -> onSuccess = jasmine.createSpy('onSuccess') waitsForPromise -> @@ -162,27 +153,47 @@ fdescribe "GitRepositoryAsync", -> expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - xdescribe ".checkoutHead(path)", -> + describe ".checkoutHead(path)", -> [filePath] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = GitRepositoryAsync.open(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() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() + + # Don't need to assert that this succeded because waitsForPromise will + # fail if it was rejected. + waitsForPromise -> + repo.checkoutHead(filePath) + + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).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 '' + waitsForPromise -> + repo.checkoutHead(filePath) + runs -> + expect(fs.readFileSync(filePath, 'utf8')).toBe '' - it "fires a status-changed event if the checkout completes successfully", -> + xit "fires a status-changed event if the checkout completes successfully", -> fs.writeFileSync(filePath, 'ch ch changes') repo.getPathStatus(filePath) statusHandler = jasmine.createSpy('statusHandler') diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f3916eb19..7d7aab570 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -63,9 +63,9 @@ module.exports = class GitRepositoryAsync { } checkoutHead (_path) { - return this.repoPromise.then(function (repo) { + return this.repoPromise.then((repo) => { var checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [_path] + checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH Git.Checkout.head(repo, checkoutOptions) }) From 58c989455ac82fdf50d4c1afb592933f5310db84 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 13:47:51 +0200 Subject: [PATCH 011/254] Add .eslintrc --- .eslintrc | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..c7d309c6c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "env": { + "es6": true + }, + "extends": "standard" +} From ab4ba2ca4ca07cb56927da8593d258043a49be48 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 15:15:42 +0200 Subject: [PATCH 012/254] .getPathStatus() --- spec/git-repository-async-spec.coffee | 42 ++++++++++++++++---------- src/git-repository-async.js | 43 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 7de2a0b01..2972db635 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -1,5 +1,6 @@ temp = require 'temp' GitRepositoryAsync = require '../src/git-repository-async' +Git = require 'nodegit' fs = require 'fs-plus' os = require 'os' path = require 'path' @@ -18,7 +19,7 @@ copyRepository = -> openFixture = (fixture)-> GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) -fdescribe "GitRepositoryAsync", -> +describe "GitRepositoryAsync", -> repo = null # beforeEach -> @@ -154,6 +155,8 @@ fdescribe "GitRepositoryAsync", -> describe ".checkoutHead(path)", -> + # XXX this is failing sporadically with various errors around not finding the + # repo / files in the repo [filePath] = [] beforeEach -> @@ -175,16 +178,17 @@ fdescribe "GitRepositoryAsync", -> runs -> expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - # Don't need to assert that this succeded because waitsForPromise will - # fail if it was rejected. + # Don't need to assert that this succeded because waitsForPromise should + # fail if it was rejected.. waitsForPromise -> repo.checkoutHead(filePath) - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() it "restores the contents of the path to the original text", -> fs.writeFileSync(filePath, 'ch ch changes') @@ -243,25 +247,33 @@ fdescribe "GitRepositoryAsync", -> repo.destroy() expect(-> repo.getShortHead()).toThrow() - xdescribe ".getPathStatus(path)", -> + describe ".getPathStatus(path)", -> [filePath] = [] beforeEach -> workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) filePath = path.join(workingDirectory, 'file.txt') it "trigger a status-changed event when the new status differs from the last cached one", -> statusHandler = jasmine.createSpy("statusHandler") repo.onDidChangeStatus statusHandler fs.writeFileSync(filePath, '') - 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 + waitsForPromise -> + repo.getPathStatus(filePath) + + runs -> + expect(statusHandler.callCount).toBe 1 + status = Git.Status.STATUS.WT_MODIFIED + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} + fs.writeFileSync(filePath, 'abc') + + waitsForPromise -> + status = repo.getPathStatus(filePath) + + runs -> + expect(statusHandler.callCount).toBe 1 xdescribe ".getDirectoryStatus(path)", -> [directoryPath, filePath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 7d7aab570..d99948afc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -2,6 +2,7 @@ const Git = require('nodegit') const path = require('path') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') // GitUtils is temporarily used for ::relativize only, because I don't want // to port it just yet. TODO: remove @@ -15,10 +16,17 @@ module.exports = class GitRepositoryAsync { constructor (path) { this.repo = null + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + this.pathStatusCache = {} this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) } + destroy () { + this.subscriptions.dispose() + } + getPath () { return this.repoPromise.then((repo) => { return Promise.resolve(repo.path().replace(/\/$/, '')) @@ -38,6 +46,7 @@ module.exports = class GitRepositoryAsync { basePath = repo.workdir() return repo.getStatus() }).then((statuses) => { + console.log('statuses', statuses) return statuses.filter(function (status) { return _path === path.join(basePath, status.path()) }) @@ -70,4 +79,38 @@ module.exports = class GitRepositoryAsync { Git.Checkout.head(repo, checkoutOptions) }) } + + // Returns a Promise that resolves to the status bit of a given path if it has + // one, otherwise 'current'. + getPathStatus (_path) { + var relativePath = this._gitUtilsRepo.relativize(_path) + return this.repoPromise.then((repo) => { + return this._filterStatusesByPath(_path) + }).then((statuses) => { + var cachedStatus = this.pathStatusCache[relativePath] || 0 + var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + console.log('cachedStatus', cachedStatus, 'status', status) + if (status != cachedStatus) { + this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) + } + this.pathStatusCache[relativePath] = status + return Promise.resolve(status) + }) + } + + // Event subscription + // ================== + + onDidChangeStatus (callback) { + return this.emitter.on('did-change-status', callback) + } + + onDidChangeStatuses (callback) { + return this.emitter.on('did-change-statuses', callback) + } + + onDidDestroy (callback) { + return this.emitter.on('did-destroy', callback) + } + } From 2c96f8ce3d9d79cd82d55c397a06c95a7b61fcef Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 16:10:13 +0200 Subject: [PATCH 013/254] .getDirectoryStatus --- spec/git-repository-async-spec.coffee | 26 +++++++--- src/git-repository-async.js | 75 +++++++++++++++++++++------ 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 2972db635..fc9bb3f96 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -275,20 +275,34 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - xdescribe ".getDirectoryStatus(path)", -> + describe ".getDirectoryStatus(path)", -> [directoryPath, filePath] = [] beforeEach -> workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) + repo = GitRepositoryAsync.open(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 + onSuccess = jasmine.createSpy('onSuccess') + onSuccess2 = jasmine.createSpy('onSuccess2') + + waitsForPromise -> + repo.getDirectoryStatus(directoryPath).then(onSuccess) + + runs -> + expect(onSuccess.callCount).toBe 1 + console.log onSuccess.mostRecentCall.args + expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false + fs.writeFileSync(filePath, 'abc') + + waitsForPromise -> + repo.getDirectoryStatus(directoryPath).then(onSuccess2) + runs -> + expect(onSuccess2.callCount).toBe 1 + expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true + xdescribe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d99948afc..b4399e138 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -39,20 +39,6 @@ module.exports = class GitRepositoryAsync { }) } - _filterStatusesByPath (_path) { - // Surely I'm missing a built-in way to do this - var basePath = null - return this.repoPromise.then((repo) => { - basePath = repo.workdir() - return repo.getStatus() - }).then((statuses) => { - console.log('statuses', statuses) - return statuses.filter(function (status) { - return _path === path.join(basePath, status.path()) - }) - }) - } - isPathModified (_path) { return this._filterStatusesByPath(_path).then(function (statuses) { var ret = statuses.filter((status) => { @@ -90,7 +76,7 @@ module.exports = class GitRepositoryAsync { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT console.log('cachedStatus', cachedStatus, 'status', status) - if (status != cachedStatus) { + if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status @@ -98,6 +84,65 @@ module.exports = class GitRepositoryAsync { }) } + // Get the status of a directory in the repository's working directory. + // + // * `directoryPath` The {String} path to check. + // + // Returns a promise resolving to a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + + getDirectoryStatus (directoryPath) { + var relativePath = this._gitUtilsRepo.relativize(directoryPath) + return this.repoPromise.then((repo) => { + return this._filterStatusesByDirectory(relativePath) + }).then((statuses) => { + return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { + var ret = 0 + var filteredBits = bits.filter(function (b) { return b > 0 }) + if (filteredBits.length > 0) { + ret = filteredBits.pop() + } + return Promise.resolve(ret) + }) + }) + } + + // Utility functions + // ================= + + // TODO fix with bitwise ops + isStatusNew (statusBit) { + return statusBit === Git.Status.STATUS.WT_NEW || statusBit === Git.Status.STATUS.INDEX_NEW + } + + isStatusModified (statusBit) { + return statusBit === Git.Status.STATUS.WT_MODIFIED || statusBit === Git.Status.STATUS.INDEX_MODIFIED + } + + _filterStatusesByPath (_path) { + // Surely I'm missing a built-in way to do this + var basePath = null + return this.repoPromise.then((repo) => { + basePath = repo.workdir() + return repo.getStatus() + }).then((statuses) => { + console.log('statuses', statuses) + return statuses.filter(function (status) { + return _path === path.join(basePath, status.path()) + }) + }) + } + + _filterStatusesByDirectory (directoryPath) { + return this.repoPromise.then(function (repo) { + return repo.getStatus() + }).then(function (statuses) { + var filtered = statuses.filter((status) => { + return status.path().indexOf(directoryPath) === 0 + }) + return Promise.resolve(filtered) + }) + } // Event subscription // ================== From 0e6d30e707fd1b9c1791fd1de19538db4000f927 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 17:04:27 +0200 Subject: [PATCH 014/254] .refreshStatus() (partial implementation) --- spec/git-repository-async-spec.coffee | 15 ++++++----- src/git-repository-async.js | 38 ++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index fc9bb3f96..55d4087d8 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -293,7 +293,6 @@ describe "GitRepositoryAsync", -> runs -> expect(onSuccess.callCount).toBe 1 - console.log onSuccess.mostRecentCall.args expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false fs.writeFileSync(filePath, 'abc') @@ -304,12 +303,12 @@ describe "GitRepositoryAsync", -> expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true - xdescribe ".refreshStatus()", -> + describe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] beforeEach -> workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) modifiedPath = path.join(workingDirectory, 'file.txt') newPath = path.join(workingDirectory, 'untracked.txt') cleanPath = path.join(workingDirectory, 'other.txt') @@ -320,13 +319,15 @@ describe "GitRepositoryAsync", -> it "returns status information for all new and modified files", -> fs.writeFileSync(modifiedPath, 'making this path modified') statusHandler = jasmine.createSpy('statusHandler') + onSuccess = jasmine.createSpy('onSuccess') repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 + waitsForPromise -> + repo.refreshStatus().then(onSuccess) runs -> + # Callers will use the promise returned by refreshStatus, not the + # cache directly + expect(onSuccess.mostRecentCall.args[0]).toEqual(repo.pathStatusCache) expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b4399e138..34f91de15 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -4,9 +4,13 @@ const Git = require('nodegit') const path = require('path') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +// Temporary requires +// ================== // GitUtils is temporarily used for ::relativize only, because I don't want // to port it just yet. TODO: remove const GitUtils = require('git-utils') +// Just using this for _.isEqual and _.object, we should impl our own here +const _ = require('underscore-plus') module.exports = class GitRepositoryAsync { static open (path) { @@ -75,7 +79,6 @@ module.exports = class GitRepositoryAsync { }).then((statuses) => { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - console.log('cachedStatus', cachedStatus, 'status', status) if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } @@ -93,6 +96,7 @@ module.exports = class GitRepositoryAsync { getDirectoryStatus (directoryPath) { var relativePath = this._gitUtilsRepo.relativize(directoryPath) + // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { @@ -107,16 +111,43 @@ module.exports = class GitRepositoryAsync { }) } + // Refreshes the git status. Note: the sync GitRepository class does this with + // a separate process, let's see if we can avoid that. + refreshStatus () { + // TODO add upstream, branch, and submodule tracking + return this.repoPromise.then((repo) => { + return repo.getStatus() + }).then((statuses) => { + // update the status cache + return Promise.all(statuses.map((status) => { + return [status.path(), status.statusBit()] + })).then((statusesByPath) => { + var newPathStatusCache = _.object(statusesByPath) + return Promise.resolve(newPathStatusCache) + }) + }).then((newPathStatusCache) => { + if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { + this.emitter.emit('did-change-statuses') + } + this.pathStatusCache = newPathStatusCache + return Promise.resolve(newPathStatusCache) + }) + } + // Utility functions // ================= + getCachedPathStatus (_path) { + return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] + } + // TODO fix with bitwise ops isStatusNew (statusBit) { - return statusBit === Git.Status.STATUS.WT_NEW || statusBit === Git.Status.STATUS.INDEX_NEW + return Object.is(statusBit, Git.Status.STATUS.WT_NEW) || Object.is(statusBit, Git.Status.STATUS.INDEX_NEW) } isStatusModified (statusBit) { - return statusBit === Git.Status.STATUS.WT_MODIFIED || statusBit === Git.Status.STATUS.INDEX_MODIFIED + return Object.is(statusBit, Git.Status.STATUS.WT_MODIFIED) || Object.is(statusBit, Git.Status.STATUS.INDEX_MODIFIED) } _filterStatusesByPath (_path) { @@ -126,7 +157,6 @@ module.exports = class GitRepositoryAsync { basePath = repo.workdir() return repo.getStatus() }).then((statuses) => { - console.log('statuses', statuses) return statuses.filter(function (status) { return _path === path.join(basePath, status.path()) }) From 60180677a20d4f49548de99812245aa13e1630a1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 13:59:58 +0200 Subject: [PATCH 015/254] add .async attribute to GitRepository --- spec/git-spec.coffee | 10 ++++++++++ src/git-repository.coffee | 3 +++ 2 files changed, 13 insertions(+) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index a72cd47cd..edabd0722 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -25,6 +25,16 @@ describe "GitRepository", -> it "returns null when no repository is found", -> expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() + describe ".async", -> + it "returns a GitRepositoryAsync for the same repo", -> + repoPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + repo = new GitRepository(repoPath) + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.async.getPath().then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath) + describe "new GitRepository(path)", -> it "throws an exception when no repository is found", -> expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 064b86dd3..724142264 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -3,6 +3,7 @@ _ = require 'underscore-plus' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' fs = require 'fs-plus' +GitRepositoryAsync = require './git-repository-async' GitUtils = require 'git-utils' Task = require './task' @@ -75,6 +76,8 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") + @async = GitRepositoryAsync.open(path) + @statuses = {} @upstream = {ahead: 0, behind: 0} for submodulePath, submoduleRepo of @repo.submodules From 2ff283a6b0b820d855b6a466bd61a06177832712 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:01:35 +0200 Subject: [PATCH 016/254] Async buffer events --- spec/git-repository-async-spec.coffee | 57 +++++++++++++++++++------- src/git-repository-async.js | 58 ++++++++++++++++++++++++--- src/git-repository.coffee | 6 ++- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 55d4087d8..3bab16bf7 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -332,7 +332,11 @@ describe "GitRepositoryAsync", -> expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - xdescribe "buffer events", -> + # This tests the async implementation's events directly, but ultimately I + # think we want users to just be able to subscribe to events on GitRepository + # and have them bubble up from async-land + + describe "buffer events", -> [editor] = [] beforeEach -> @@ -345,32 +349,57 @@ describe "GitRepositoryAsync", -> editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler + repo = atom.project.getRepositories()[0] + repo.async.onDidChangeStatus statusHandler editor.save() - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} it "emits a status-changed event when a buffer is reloaded", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler editor.getBuffer().reload() - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - editor.getBuffer().reload() - expect(statusHandler.callCount).toBe 1 + reloadHandler = jasmine.createSpy 'reloadHandler' + + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() + + waitsFor -> + reloadHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "emits a status-changed event when a buffer's path changes", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler + atom.project.getRepositories()[0].async.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 + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + + pathHandler = jasmine.createSpy('pathHandler') + buffer = editor.getBuffer() + buffer.onDidChangePath pathHandler + buffer.emitter.emit 'did-change-path' + waitsFor -> + pathHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "stops listening to the buffer when the repository is destroyed (regression)", -> atom.project.getRepositories()[0].destroy() diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 34f91de15..cbc040bbc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -13,18 +13,35 @@ const GitUtils = require('git-utils') const _ = require('underscore-plus') module.exports = class GitRepositoryAsync { - static open (path) { + static open (path, options = {}) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? - return new GitRepositoryAsync(path) + return new GitRepositoryAsync(path, options) } - constructor (path) { + constructor (path, options) { this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) + + var {project, refreshOnWindowFocus} = options + this.project = project + if (refreshOnWindowFocus === undefined) { + refreshOnWindowFocus = true + } + if (refreshOnWindowFocus) { + // TODO + } + + if (this.project) { + this.subscriptions.add(this.project.onDidAddBuffer((buffer) => { + this.subscribeToBuffer(buffer) + })) + + this.project.getBuffers().forEach((buffer) => { this.subscribeToBuffer(buffer) }) + } } destroy () { @@ -73,6 +90,7 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { + console.log('getting path status for', _path) var relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { return this._filterStatusesByPath(_path) @@ -80,6 +98,7 @@ module.exports = class GitRepositoryAsync { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { + console.log('async emitting', {path: _path, pathStatus: status}) this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status @@ -134,8 +153,37 @@ module.exports = class GitRepositoryAsync { }) } - // Utility functions - // ================= + // Section: Private + // ================ + + subscribeToBuffer (buffer) { + var getBufferPathStatus = () => { + var _path = buffer.getPath() + var bufferSubscriptions = new CompositeDisposable() + + if (_path) { + // We don't need to do anything with this promise, we just want the + // emitted event side effect + this.getPathStatus(_path) + } + + bufferSubscriptions.add( + buffer.onDidSave(getBufferPathStatus), + buffer.onDidReload(getBufferPathStatus), + buffer.onDidChangePath(getBufferPathStatus) + ) + + bufferSubscriptions.add(() => { + buffer.onDidDestroy(() => { + bufferSubscriptions.dispose() + this.subscriptions.remove(bufferSubscriptions) + }) + }) + + this.subscriptions.add(bufferSubscriptions) + return + } + } getCachedPathStatus (_path) { return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 724142264..a2e28b127 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -76,7 +76,7 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - @async = GitRepositoryAsync.open(path) + @async = GitRepositoryAsync.open(path, options) @statuses = {} @upstream = {ahead: 0, behind: 0} @@ -319,6 +319,9 @@ class GitRepository # Returns a {Number} representing the status. This value can be passed to # {::isStatusModified} or {::isStatusNew} to get more information. getPathStatus: (path) -> + # Trigger events emitted on the async repo as well + @async.getPathStatus(path) + repo = @getRepo(path) relativePath = @relativize(path) currentPathStatus = @statuses[relativePath] ? 0 @@ -479,6 +482,7 @@ class GitRepository # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. refreshStatus: -> + @async.refreshStatus() @handlerPath ?= require.resolve('./repository-status-handler') @statusTask?.terminate() From 716a96f81494d349ddf8ec9f969f891465f709d4 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:02:34 +0200 Subject: [PATCH 017/254] Errant logs --- src/git-repository-async.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index cbc040bbc..543d1e610 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -90,7 +90,6 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - console.log('getting path status for', _path) var relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { return this._filterStatusesByPath(_path) @@ -98,7 +97,6 @@ module.exports = class GitRepositoryAsync { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { - console.log('async emitting', {path: _path, pathStatus: status}) this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status From fbd0c3677c60bcdbf485baee03ec4176e075d0aa Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:40:11 +0200 Subject: [PATCH 018/254] Fix up deserialize specs --- spec/git-repository-async-spec.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 3bab16bf7..581872053 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -405,7 +405,7 @@ describe "GitRepositoryAsync", -> atom.project.getRepositories()[0].destroy() expect(-> editor.save()).not.toThrow() - xdescribe "when a project is deserialized", -> + describe "when a project is deserialized", -> [buffer, project2] = [] afterEach -> @@ -429,7 +429,10 @@ describe "GitRepositoryAsync", -> buffer.append('changes') statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].onDidChangeStatus statusHandler + project2.getRepositories()[0].async.onDidChangeStatus statusHandler buffer.save() - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} From 057f0817e392e3c5e7895628bb755958b2ac5236 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:40:11 +0200 Subject: [PATCH 019/254] Fix up deserialize specs From 95b3ac806f3b72c0d854e71a4009eef5495c8fd6 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:42:20 +0200 Subject: [PATCH 020/254] un-nest waitsForPromise --- spec/git-repository-async-spec.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 581872053..8ca7c861f 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -182,13 +182,13 @@ describe "GitRepositoryAsync", -> # fail if it was rejected.. waitsForPromise -> repo.checkoutHead(filePath) - runs -> onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() it "restores the contents of the path to the original text", -> fs.writeFileSync(filePath, 'ch ch changes') From 49969288bb54ff8a47aac232c00b43345e2366eb Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:42:20 +0200 Subject: [PATCH 021/254] un-nest waitsForPromise From 196fb35b6553fd978839916994fc3b857ff3279c Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:52:43 +0200 Subject: [PATCH 022/254] Add missing return in checkouthead --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 543d1e610..e3bf0e1ab 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -83,7 +83,7 @@ module.exports = class GitRepositoryAsync { var checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - Git.Checkout.head(repo, checkoutOptions) + return Git.Checkout.head(repo, checkoutOptions) }) } From 8ff31bd3dcc5ab0b2f677af3fa94ec6a667460eb Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:53:32 +0200 Subject: [PATCH 023/254] Remove no longer relevant comment --- spec/git-repository-async-spec.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 8ca7c861f..f5adf23ea 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -155,8 +155,6 @@ describe "GitRepositoryAsync", -> describe ".checkoutHead(path)", -> - # XXX this is failing sporadically with various errors around not finding the - # repo / files in the repo [filePath] = [] beforeEach -> From 4c0c732766f19f1cd4d82894e66f4d3ce4046172 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 17:03:08 +0200 Subject: [PATCH 024/254] Get path status after checkout --- spec/git-repository-async-spec.coffee | 24 ++++++++++++++++-------- src/git-repository-async.js | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index f5adf23ea..1506d3453 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -195,17 +195,25 @@ describe "GitRepositoryAsync", -> runs -> expect(fs.readFileSync(filePath, 'utf8')).toBe '' - xit "fires a status-changed event if the checkout completes successfully", -> + it "fires a did-change-status event if the checkout completes successfully", -> fs.writeFileSync(filePath, 'ch ch changes') - repo.getPathStatus(filePath) 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 + waitsForPromise -> + repo.getPathStatus(filePath) + runs -> + repo.onDidChangeStatus statusHandler + + waitsForPromise -> + repo.checkoutHead(filePath) + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} + + waitsForPromise -> + repo.checkoutHead(filePath) + runs -> + expect(statusHandler.callCount).toBe 1 xdescribe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e3bf0e1ab..47f1c0a9b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -84,6 +84,8 @@ module.exports = class GitRepositoryAsync { checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) + }).then(() => { + return this.getPathStatus(_path) }) } From bea002bddbafbc0991cd287ed9e8dfde5d764720 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 18:02:47 +0200 Subject: [PATCH 025/254] Incomplete implementation of checkoutHeadForEditor --- spec/git-repository-async-spec.coffee | 13 +++++----- src/git-repository-async.js | 34 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 1506d3453..81dd27cd4 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -215,12 +215,12 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - xdescribe ".checkoutHeadForEditor(editor)", -> + fdescribe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') @@ -242,10 +242,11 @@ describe "GitRepositoryAsync", -> spyOn(atom, 'confirm') atom.config.set('editor.confirmCheckoutHeadRevision', false) - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - expect(atom.confirm).not.toHaveBeenCalled() + waitsForPromise -> + repo.checkoutHeadForEditor(editor) + runs -> + expect(fs.readFileSync(filePath, 'utf8')).toBe '' + expect(atom.confirm).not.toHaveBeenCalled() xdescribe ".destroy()", -> it "throws an exception when any method is called after it is called", -> diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 47f1c0a9b..53e592998 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,4 +1,5 @@ 'use babel' +const atom = window.atom const Git = require('nodegit') const path = require('path') @@ -89,6 +90,39 @@ module.exports = class GitRepositoryAsync { }) } + checkoutHeadForEditor (editor) { + var filePath = editor.getPath() + if (!filePath) { + return Promise.reject('editor.filePath() is empty') + } + + var fileName = path.basename(filePath) + var checkoutHead = () => { + if (editor.buffer.isModified()) { + editor.buffer.reload() + } + return this.checkoutHead(filePath) + } + + var confirmCheckout = function () { + // This is bad tho + return Promise.resolve(atom.confirm({ + message: 'Confirm Checkout HEAD Revision', + detailedMessage: `Are you sure you want to discard all changes to "${fileName}" since the last Git commit?`, + buttons: { + OK: checkoutHead, + Cancel: null + } + })) + } + + if (atom.config.get('editor.confirmCheckoutHeadRevision')) { + return confirmCheckout() + } else { + return checkoutHead() + } + } + // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { From b359049b9d590cbaf3391a291cc2334ce4cc21c1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 18:05:54 +0200 Subject: [PATCH 026/254] Fix confirm dialog spec --- spec/git-repository-async-spec.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 81dd27cd4..667da12ef 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -215,7 +215,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - fdescribe ".checkoutHeadForEditor(editor)", -> + describe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] beforeEach -> @@ -234,9 +234,10 @@ describe "GitRepositoryAsync", -> spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() atom.config.set('editor.confirmCheckoutHeadRevision', true) - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' + waitsForPromise -> + repo.checkoutHeadForEditor(editor) + runs -> + expect(fs.readFileSync(filePath, 'utf8')).toBe '' it "does not display a dialog when confirmation is disabled", -> spyOn(atom, 'confirm') From 7b87764992057d9798e6c7aa6c9c659974f53440 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 21 Oct 2015 17:09:32 +0200 Subject: [PATCH 027/254] this breaks under the new test env setup --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 53e592998..41ca6d05e 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,5 +1,4 @@ 'use babel' -const atom = window.atom const Git = require('nodegit') const path = require('path') From 9f0def76f53125d03bdfc5ed64acb64e76da1ca2 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 21 Oct 2015 17:39:36 +0200 Subject: [PATCH 028/254] Project.serialize is now Project::serialize - cf https://github.com/atom/atom/commit/26f0ef5424ba3e7e45dcb5a9a56f47348119678d --- spec/git-repository-async-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 667da12ef..5cd8e22d1 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -426,7 +426,7 @@ describe "GitRepositoryAsync", -> atom.workspace.open('file.txt') runs -> - project2 = Project.deserialize(atom.project.serialize()) + project2 = atom.project.deserialize(atom.project.serialize(), atom.deserializers) buffer = project2.getBuffers()[0] waitsFor -> From 11ccb980fed0fbe178ceb418ef9498404917dc73 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 21 Oct 2015 17:53:24 +0200 Subject: [PATCH 029/254] use deserialize api right --- spec/git-repository-async-spec.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 5cd8e22d1..6c4f54639 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -426,7 +426,8 @@ describe "GitRepositoryAsync", -> atom.workspace.open('file.txt') runs -> - project2 = atom.project.deserialize(atom.project.serialize(), atom.deserializers) + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + project2.deserialize(atom.project.serialize(), atom.deserializers) buffer = project2.getBuffers()[0] waitsFor -> From c2520f490956c8757cb426ac243a142bbb4fed9e Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 22 Oct 2015 15:42:42 +0200 Subject: [PATCH 030/254] Remove confirmation from git-repo-async to match git-repo cf https://github.com/atom/atom/commit/f9a269ed995c4151678aef787573fe44657e d6dd --- src/git-repository-async.js | 41 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 41ca6d05e..2f45a016a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -90,36 +90,19 @@ module.exports = class GitRepositoryAsync { } checkoutHeadForEditor (editor) { - var filePath = editor.getPath() - if (!filePath) { - return Promise.reject('editor.filePath() is empty') - } - - var fileName = path.basename(filePath) - var checkoutHead = () => { - if (editor.buffer.isModified()) { - editor.buffer.reload() - } - return this.checkoutHead(filePath) - } - - var confirmCheckout = function () { - // This is bad tho - return Promise.resolve(atom.confirm({ - message: 'Confirm Checkout HEAD Revision', - detailedMessage: `Are you sure you want to discard all changes to "${fileName}" since the last Git commit?`, - buttons: { - OK: checkoutHead, - Cancel: null + return new Promise(function (resolve, reject) { + var filePath = editor.getPath() + if (filePath) { + if (editor.buffer.isModified()) { + editor.buffer.reload() } - })) - } - - if (atom.config.get('editor.confirmCheckoutHeadRevision')) { - return confirmCheckout() - } else { - return checkoutHead() - } + resolve(filePath) + } else { + reject() + } + }).then((filePath) => { + return this.checkoutHead(filePath) + }) } // Returns a Promise that resolves to the status bit of a given path if it has From 02d8ead883dfd32cb9274fafa296225bfce40462 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 22 Oct 2015 15:49:21 +0200 Subject: [PATCH 031/254] Checkout head asyncly This is the first tentative step in actually using the nodegit wrapper. --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 30d5694c4..786965d41 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -646,7 +646,7 @@ class TextEditor extends Model checkoutHead = => @project.repositoryForDirectory(new Directory(path.dirname(filePath))) .then (repository) => - repository?.checkoutHeadForEditor(this) + repository?.async.checkoutHeadForEditor(this) if @config.get('editor.confirmCheckoutHeadRevision') @applicationDelegate.confirm From 22ff8c5a9ee71ed22e3e4af4d76a5a21de4afc0c Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 23 Oct 2015 12:42:00 +0200 Subject: [PATCH 032/254] don't return a promise here --- src/git-repository-async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 2f45a016a..e1e9e55cf 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -62,10 +62,9 @@ module.exports = class GitRepositoryAsync { isPathModified (_path) { return this._filterStatusesByPath(_path).then(function (statuses) { - var ret = statuses.filter((status) => { + return statuses.filter((status) => { return status.isModified() }).length > 0 - return Promise.resolve(ret) }) } From 1d2834f2ade80a1ce7208193f5c7b47bdd9068d1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 23 Oct 2015 12:42:16 +0200 Subject: [PATCH 033/254] .isProjectAtRoot --- src/git-repository-async.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e1e9e55cf..b475f3467 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -251,4 +251,21 @@ module.exports = class GitRepositoryAsync { return this.emitter.on('did-destroy', callback) } + // + // Section: Repository Details + // + + // Returns a {Promise} that resolves true if at the root, false if in a + // subfolder of the repository. + isProjectAtRoot () { + if (this.projectAtRoot === undefined) { + this.projectAtRoot = Promise.resolve(() => { + return this.repoPromise.then((repo) => { + return this.project.relativize(repo.workdir) + }) + }) + } + + return this.projectAtRoot + } } From e01a699d74a1b86de21c2ce5893fff8d7d7641ee Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:17:48 +0100 Subject: [PATCH 034/254] Export GitRepositoryAsync from the Atom global Mostly to have a handle to the nodegit repo statuses. --- exports/atom.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/exports/atom.coffee b/exports/atom.coffee index c3601e1cc..59ac090d1 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -8,6 +8,7 @@ module.exports = BufferedNodeProcess: require '../src/buffered-node-process' BufferedProcess: require '../src/buffered-process' GitRepository: require '../src/git-repository' + GitRepositoryAsync: require '../src/git-repository-async' Notification: require '../src/notification' TextBuffer: TextBuffer Point: Point From 03045674b86491f9ed04f364300052c5c6247a75 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:18:11 +0100 Subject: [PATCH 035/254] Export nodegit from GitRepositoryAsync --- src/git-repository-async.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b475f3467..52e974eb0 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -18,6 +18,10 @@ module.exports = class GitRepositoryAsync { return new GitRepositoryAsync(path, options) } + static get Git () { + return Git + } + constructor (path, options) { this.repo = null this.emitter = new Emitter() From 86f6b6217676a3424b1acff85cc970d27e4d606f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:18:37 +0100 Subject: [PATCH 036/254] Correctly determine new/mod/del status as per git-utils --- src/git-repository-async.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 52e974eb0..ada5fbd61 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -4,6 +4,11 @@ const Git = require('nodegit') const path = require('path') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE +const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW +const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED +const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE + // Temporary requires // ================== // GitUtils is temporarily used for ::relativize only, because I don't want @@ -133,18 +138,21 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { - var relativePath = this._gitUtilsRepo.relativize(directoryPath) + let relativePath = this._gitUtilsRepo.relativize(directoryPath) // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { - var ret = 0 - var filteredBits = bits.filter(function (b) { return b > 0 }) + let directoryStatus = 0 + let filteredBits = bits.filter(function (b) { return b > 0 }) if (filteredBits.length > 0) { - ret = filteredBits.pop() + filteredBits.forEach(function (bit) { + directoryStatus |= bit + }) } - return Promise.resolve(ret) + + return directoryStatus }) }) } @@ -208,13 +216,24 @@ module.exports = class GitRepositoryAsync { return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] } - // TODO fix with bitwise ops isStatusNew (statusBit) { - return Object.is(statusBit, Git.Status.STATUS.WT_NEW) || Object.is(statusBit, Git.Status.STATUS.INDEX_NEW) + return (statusBit & newStatusFlags) > 0 } isStatusModified (statusBit) { - return Object.is(statusBit, Git.Status.STATUS.WT_MODIFIED) || Object.is(statusBit, Git.Status.STATUS.INDEX_MODIFIED) + return (statusBit & modifiedStatusFlags) > 0 + } + + isStatusStaged (statusBit) { + return (statusBit & indexStatusFlags) > 0 + } + + isStatusIgnored (statusBit) { + return (statusBit & (1 << 14)) > 0 + } + + isStatusDeleted (statusBit) { + return (statusBit & deletedStatusFlags) > 0 } _filterStatusesByPath (_path) { From e39fe437ec269033ac2dc0fae66f1b16c556ba67 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:28:48 +0100 Subject: [PATCH 037/254] Replace a bunch of Promise.resolve return values with plain values --- src/git-repository-async.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index ada5fbd61..f5239fbbd 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -59,13 +59,13 @@ module.exports = class GitRepositoryAsync { getPath () { return this.repoPromise.then((repo) => { - return Promise.resolve(repo.path().replace(/\/$/, '')) + return repo.path().replace(/\/$/, '') }) } isPathIgnored (_path) { return this.repoPromise.then((repo) => { - return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) + Git.Ignore.pathIsIgnored(repo, _path) }) } @@ -79,10 +79,9 @@ module.exports = class GitRepositoryAsync { isPathNew (_path) { return this._filterStatusesByPath(_path).then(function (statuses) { - var ret = statuses.filter((status) => { + return statuses.filter((status) => { return status.isNew() }).length > 0 - return Promise.resolve(ret) }) } @@ -126,7 +125,7 @@ module.exports = class GitRepositoryAsync { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status - return Promise.resolve(status) + return status }) } @@ -169,14 +168,14 @@ module.exports = class GitRepositoryAsync { return [status.path(), status.statusBit()] })).then((statusesByPath) => { var newPathStatusCache = _.object(statusesByPath) - return Promise.resolve(newPathStatusCache) + return newPathStatusCache }) }).then((newPathStatusCache) => { if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { this.emitter.emit('did-change-statuses') } this.pathStatusCache = newPathStatusCache - return Promise.resolve(newPathStatusCache) + return newPathStatusCache }) } @@ -256,7 +255,7 @@ module.exports = class GitRepositoryAsync { var filtered = statuses.filter((status) => { return status.path().indexOf(directoryPath) === 0 }) - return Promise.resolve(filtered) + return filtered }) } // Event subscription From 4a478f7f84dbac3ee0ff25e1ca5efe6fab9bd327 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 16:35:48 +0100 Subject: [PATCH 038/254] ES6 style fixes --- src/git-repository-async.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f5239fbbd..79ebf359f 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -35,7 +35,7 @@ module.exports = class GitRepositoryAsync { this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) - var {project, refreshOnWindowFocus} = options + let {project, refreshOnWindowFocus} = options this.project = project if (refreshOnWindowFocus === undefined) { refreshOnWindowFocus = true @@ -87,7 +87,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { - var checkoutOptions = new Git.CheckoutOptions() + let checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) @@ -98,7 +98,7 @@ module.exports = class GitRepositoryAsync { checkoutHeadForEditor (editor) { return new Promise(function (resolve, reject) { - var filePath = editor.getPath() + let filePath = editor.getPath() if (filePath) { if (editor.buffer.isModified()) { editor.buffer.reload() @@ -115,12 +115,12 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - var relativePath = this._gitUtilsRepo.relativize(_path) + let relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { return this._filterStatusesByPath(_path) }).then((statuses) => { - var cachedStatus = this.pathStatusCache[relativePath] || 0 - var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + let cachedStatus = this.pathStatusCache[relativePath] || 0 + let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } @@ -167,8 +167,7 @@ module.exports = class GitRepositoryAsync { return Promise.all(statuses.map((status) => { return [status.path(), status.statusBit()] })).then((statusesByPath) => { - var newPathStatusCache = _.object(statusesByPath) - return newPathStatusCache + return _.object(statusesByPath) }) }).then((newPathStatusCache) => { if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { @@ -183,9 +182,9 @@ module.exports = class GitRepositoryAsync { // ================ subscribeToBuffer (buffer) { - var getBufferPathStatus = () => { - var _path = buffer.getPath() - var bufferSubscriptions = new CompositeDisposable() + let getBufferPathStatus = () => { + let _path = buffer.getPath() + let bufferSubscriptions = new CompositeDisposable() if (_path) { // We don't need to do anything with this promise, we just want the @@ -237,7 +236,7 @@ module.exports = class GitRepositoryAsync { _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this - var basePath = null + let basePath = null return this.repoPromise.then((repo) => { basePath = repo.workdir() return repo.getStatus() @@ -252,10 +251,9 @@ module.exports = class GitRepositoryAsync { return this.repoPromise.then(function (repo) { return repo.getStatus() }).then(function (statuses) { - var filtered = statuses.filter((status) => { + return statuses.filter((status) => { return status.path().indexOf(directoryPath) === 0 }) - return filtered }) } // Event subscription From abd41d120804f1d6c53078390f7baf149fd875ba Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 16:35:57 +0100 Subject: [PATCH 039/254] Add .isSubmodule --- src/git-repository-async.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 79ebf359f..9fd8a9e1d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -288,4 +288,21 @@ module.exports = class GitRepositoryAsync { return this.projectAtRoot } + + // Returns a {Promise} that resolves true if the given path is a submodule in + // the repository. + isSubmodule (_path) { + return this.repoPromise.then(function (repo) { + return repo.openIndex() + }).then(function (index) { + let entry = index.getByPath(_path) + let submoduleMode = 57344 // TODO compose this from libgit2 constants + + if (entry.mode === submoduleMode) { + return true + } else { + return false + } + }) + } } From 806047bd270094fba668929a36bd45c4c68f5491 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 16:38:09 +0100 Subject: [PATCH 040/254] Add missing return --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 9fd8a9e1d..6c8fdd996 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -65,7 +65,7 @@ module.exports = class GitRepositoryAsync { isPathIgnored (_path) { return this.repoPromise.then((repo) => { - Git.Ignore.pathIsIgnored(repo, _path) + return Git.Ignore.pathIsIgnored(repo, _path) }) } From 4bbf435ba86b750cc632731c2b20e6b5f89564d2 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 28 Oct 2015 11:44:57 +0100 Subject: [PATCH 041/254] drop eslintrc, add babel-eslint and standard --- .eslintrc | 6 ------ package.json | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c7d309c6c..000000000 --- a/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "env": { - "es6": true - }, - "extends": "standard" -} diff --git a/package.json b/package.json index 6f1970297..d0dd7bbb0 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,10 @@ "underscore-plus": "^1.6.6", "yargs": "^3.23.0" }, + "devDependencies" : { + "babel-eslint": "^4.1.3", + "standard": "^5.3.1" + }, "packageDependencies": { "atom-dark-syntax": "0.27.0", "atom-dark-ui": "0.51.0", From 64514fad7e19ba935f19a8155602faa5f6f54d07 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:29:34 +0100 Subject: [PATCH 042/254] Nest waitsFors to avoid jasmine 1.3 async queue flakiness, cc @nathansobo --- spec/git-repository-async-spec.coffee | 116 +++++++++++++++----------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 6c4f54639..ae79d6130 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -350,68 +350,82 @@ describe "GitRepositoryAsync", -> beforeEach -> atom.project.setPaths([copyRepository()]) - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> editor = o - it "emits a status-changed event when a buffer is saved", -> - editor.insertNewline() - - statusHandler = jasmine.createSpy('statusHandler') - repo = atom.project.getRepositories()[0] - repo.async.onDidChangeStatus statusHandler - editor.save() - waitsFor -> - statusHandler.callCount == 1 + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + editor.insertNewline() + statusHandler = jasmine.createSpy('statusHandler') + repo = atom.project.getRepositories()[0] + repo.async.onDidChangeStatus statusHandler + editor.save() + + waitsFor -> + statusHandler.callCount >= 1 + runs -> + console.log "in run block" + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} it "emits a status-changed event when a buffer is reloaded", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().reload() - reloadHandler = jasmine.createSpy 'reloadHandler' - - waitsFor -> - statusHandler.callCount == 1 + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() + fs.writeFileSync(editor.getPath(), 'changed') - waitsFor -> - reloadHandler.callCount == 1 - runs -> - expect(statusHandler.callCount).toBe 1 + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler + editor.getBuffer().reload() + reloadHandler = jasmine.createSpy 'reloadHandler' + + waitsFor -> + statusHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() + + waitsFor -> + reloadHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "emits a status-changed event when a buffer's path changes", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().emitter.emit 'did-change-path' - waitsFor -> - statusHandler.callCount == 1 + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + fs.writeFileSync(editor.getPath(), 'changed') - pathHandler = jasmine.createSpy('pathHandler') - buffer = editor.getBuffer() - buffer.onDidChangePath pathHandler - buffer.emitter.emit 'did-change-path' - waitsFor -> - pathHandler.callCount == 1 - runs -> - expect(statusHandler.callCount).toBe 1 + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler + editor.getBuffer().emitter.emit 'did-change-path' + waitsFor -> + statusHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + + pathHandler = jasmine.createSpy('pathHandler') + buffer = editor.getBuffer() + buffer.onDidChangePath pathHandler + buffer.emitter.emit 'did-change-path' + waitsFor -> + pathHandler.callCount >= 1 + runs -> + 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() + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o + runs -> + atom.project.getRepositories()[0].destroy() + expect(-> editor.save()).not.toThrow() describe "when a project is deserialized", -> [buffer, project2] = [] @@ -441,7 +455,7 @@ describe "GitRepositoryAsync", -> project2.getRepositories()[0].async.onDidChangeStatus statusHandler buffer.save() waitsFor -> - statusHandler.callCount == 1 + statusHandler.callCount >= 1 runs -> expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} From 454d6a537063f92bddfd06f593ec275fa4acc5cb Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:32:52 +0100 Subject: [PATCH 043/254] :fire: errant log --- spec/git-repository-async-spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index ae79d6130..e02797074 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -364,7 +364,6 @@ describe "GitRepositoryAsync", -> waitsFor -> statusHandler.callCount >= 1 runs -> - console.log "in run block" expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} From dcef6b301f188fb264ec71a8c474f10746eef795 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:41:19 +0100 Subject: [PATCH 044/254] Formatting --- spec/git-repository-async-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index e02797074..03c0e624b 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -353,7 +353,7 @@ describe "GitRepositoryAsync", -> it "emits a status-changed event when a buffer is saved", -> waitsForPromise -> atom.workspace.open('other.txt').then (o) -> - editor = o + editor = o runs -> editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') From 13d51c54bbca5a29cfa0c9c7504029cead537d7e Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:55:15 +0100 Subject: [PATCH 045/254] fix subscribeToBuffer method I ported this from coffee incorrectly. --- src/git-repository-async.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 6c8fdd996..cb41dde77 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -182,32 +182,30 @@ module.exports = class GitRepositoryAsync { // ================ subscribeToBuffer (buffer) { + let bufferSubscriptions = new CompositeDisposable() + let getBufferPathStatus = () => { let _path = buffer.getPath() - let bufferSubscriptions = new CompositeDisposable() - if (_path) { // We don't need to do anything with this promise, we just want the // emitted event side effect this.getPathStatus(_path) } - - bufferSubscriptions.add( - buffer.onDidSave(getBufferPathStatus), - buffer.onDidReload(getBufferPathStatus), - buffer.onDidChangePath(getBufferPathStatus) - ) - - bufferSubscriptions.add(() => { - buffer.onDidDestroy(() => { - bufferSubscriptions.dispose() - this.subscriptions.remove(bufferSubscriptions) - }) - }) - - this.subscriptions.add(bufferSubscriptions) - return } + + bufferSubscriptions.add( + buffer.onDidSave(getBufferPathStatus), + buffer.onDidReload(getBufferPathStatus), + buffer.onDidChangePath(getBufferPathStatus), + buffer.onDidDestroy(() => { + bufferSubscriptions.dispose() + this.subscriptions.remove(bufferSubscriptions) + }) + ) + + + this.subscriptions.add(bufferSubscriptions) + return } getCachedPathStatus (_path) { From 500e77dbd9a3756cf3747cf10c4880f95f87a768 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 17:37:53 +0100 Subject: [PATCH 046/254] Start async spec over in JS --- spec/git-repository-async-spec.js | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 spec/git-repository-async-spec.js diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js new file mode 100644 index 000000000..e45a9dc40 --- /dev/null +++ b/spec/git-repository-async-spec.js @@ -0,0 +1,41 @@ +'use babel' + +const path = require('path') +const temp = require('temp') + +const GitRepositoryAsync = require('../src/git-repository-async') + +const openFixture = (fixture) => { + GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) +} + + +fdescribe('GitRepositoryAsync', () => { + describe('@open(path)', () => { + it('repo is null when no repository is found', () => { + let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + + waitsForPromise({shouldReject: true}, () => { + return repo.repoPromise + }) + + runs(() => { + expect(repo.repo).toBe(null) + }) + }) + }) + + describe('.getPath()', () => { + it('returns the repository path for a repository path', () => { + let repo = openFixture('master.git') + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(repo.getPath().then(onSuccess)) + + runs(() => { + expect(onSuccess.mostRecentCall.args[0]).toBe( + path.join(__dirname, 'fixtures', 'git', 'master.git') + ) + }) + }) + }) +}) From 41d3d7a0c037a72afd22eff2980d03d6c9d20211 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 19:11:28 +0100 Subject: [PATCH 047/254] Start adding the troublesome buffer-events first --- spec/git-repository-async-spec.js | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e45a9dc40..d3cb9aaa2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -1,5 +1,6 @@ 'use babel' +const fs = require('fs-plus') const path = require('path') const temp = require('temp') @@ -9,8 +10,15 @@ const openFixture = (fixture) => { GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } +const copyRepository = () => { + let workingDirPath = temp.mkdirSync('atom-working-dir') + fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) + fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) + return fs.realpathSync(workingDirPath) +} -fdescribe('GitRepositoryAsync', () => { + +describe('GitRepositoryAsync', () => { describe('@open(path)', () => { it('repo is null when no repository is found', () => { let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) @@ -26,6 +34,8 @@ fdescribe('GitRepositoryAsync', () => { }) describe('.getPath()', () => { + xit('returns the repository path for a .git directory path') + it('returns the repository path for a repository path', () => { let repo = openFixture('master.git') let onSuccess = jasmine.createSpy('onSuccess') @@ -38,4 +48,38 @@ fdescribe('GitRepositoryAsync', () => { }) }) }) + + fdescribe('buffer events', () => { + beforeEach(() => { + // This is sync, should be fine in a beforeEach + atom.project.setPaths([copyRepository()]) + }) + + it('emits a status-changed events when a buffer is saved', () => { + let editor, called + waitsForPromise(function () { + return atom.workspace.open('other.txt').then((o) => { + editor = o + }) + }) + + runs(() => { + editor.insertNewline() + let repo = atom.project.getRepositories()[0] + repo.async.onDidChangeStatus((c) => { + called = c + }) + editor.save() + }) + + waitsFor(() => { + return Boolean(called) + }) + + runs(() => { + expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) + }) + }) + }) + }) From 9081a891e6629f79e47986f4bacadf7ca2f8b56a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 20:19:12 +0100 Subject: [PATCH 048/254] add status-changed on reload spec --- spec/git-repository-async-spec.js | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d3cb9aaa2..c9f9bc9d5 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -80,6 +80,39 @@ describe('GitRepositoryAsync', () => { expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) }) + + it('emits a status-changed event when a buffer is reloaded', () => { + let editor + let statusHandler = jasmine.createSpy('statusHandler') + let reloadHandler = jasmine.createSpy('reloadHandler') + + waitsForPromise(function () { + return atom.workspace.open('other.txt').then((o) => { + editor = o + }) + }) + + runs(() => { + fs.writeFileSync(editor.getPath(), 'changed') + atom.project.getRepositories()[0].async.onDidChangeStatus(statusHandler) + editor.getBuffer().reload() + }) + + waitsFor(() => { + return statusHandler.callCount > 0 + }) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + let buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() + }) + + waitsFor(() => { return reloadHandler.callCount > 0 }) + runs(() => { expect(statusHandler.callCount).toBe(1) }) + }) }) }) From 0c965f94390c4827018384a9ff83e3a017e6e689 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 2 Nov 2015 12:59:50 +0100 Subject: [PATCH 049/254] Flesh out ::destroy() --- src/git-repository-async.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index cb41dde77..a88ee6e6e 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -54,7 +54,15 @@ module.exports = class GitRepositoryAsync { } destroy () { - this.subscriptions.dispose() + if (this.emitter) { + this.emitter.emit('did-destroy') + this.emitter.dispose() + this.emitter = null + } + if (this.subscriptions) { + this.subscriptions.dispose() + this.subscriptions = null + } } getPath () { From da0b129c83147d7bb8799398218089c272bb5bb6 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 3 Nov 2015 16:36:24 +0100 Subject: [PATCH 050/254] Add standardjs config to package.json --- package.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package.json b/package.json index 2de9c360f..2959a8020 100644 --- a/package.json +++ b/package.json @@ -157,5 +157,21 @@ "scripts": { "preinstall": "node -e 'process.exit(0)'", "test": "node script/test" + }, + "standard": { + "parser": "babel-eslint", + "globals": [ + "atom", + "afterEach", + "beforeEach", + "describe", + "expect", + "it", + "jasmine", + "runs", + "spyOn", + "waitsFor", + "waitsForPromise" + ] } } From 47edd1b9849e123c42e29398e0f77ffbfe2644d9 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 3 Nov 2015 16:39:01 +0100 Subject: [PATCH 051/254] =?UTF-8?q?Use=20async=E2=80=99s=20destroy=20callb?= =?UTF-8?q?ack=20in=20repo=20provider.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/git-repository-provider.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-provider.coffee b/src/git-repository-provider.coffee index 593324d0c..463e2bda2 100644 --- a/src/git-repository-provider.coffee +++ b/src/git-repository-provider.coffee @@ -77,7 +77,7 @@ class GitRepositoryProvider unless repo repo = GitRepository.open(gitDirPath, {@project, @config}) return null unless repo - repo.onDidDestroy(=> delete @pathToRepository[gitDirPath]) + repo.async.onDidDestroy(=> delete @pathToRepository[gitDirPath]) @pathToRepository[gitDirPath] = repo repo.refreshIndex() repo.refreshStatus() From 4f06509fde3ae23e214a8c8f9102acc9fa20fa1a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 3 Nov 2015 16:39:26 +0100 Subject: [PATCH 052/254] Destroy async repo from sync repo --- src/git-repository.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 9b46cf04d..46d379f98 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -120,6 +120,10 @@ class GitRepository @subscriptions.dispose() @subscriptions = null + if @async? + @async.destroy() + @async = null + # Public: Invoke the given callback when this GitRepository's destroy() method # is invoked. # From 009951e757ced8d4927d049ebdd210dfd16df525 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 6 Nov 2015 14:18:59 -0500 Subject: [PATCH 053/254] Get these tests going. --- spec/git-repository-async-spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index c9f9bc9d5..2a34a83b8 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -4,10 +4,12 @@ const fs = require('fs-plus') const path = require('path') const temp = require('temp') +temp.track() + const GitRepositoryAsync = require('../src/git-repository-async') const openFixture = (fixture) => { - GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) + return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } const copyRepository = () => { @@ -18,7 +20,7 @@ const copyRepository = () => { } -describe('GitRepositoryAsync', () => { +fdescribe('GitRepositoryAsync', () => { describe('@open(path)', () => { it('repo is null when no repository is found', () => { let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) @@ -39,7 +41,7 @@ describe('GitRepositoryAsync', () => { it('returns the repository path for a repository path', () => { let repo = openFixture('master.git') let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(repo.getPath().then(onSuccess)) + waitsForPromise(() => repo.getPath().then(onSuccess)) runs(() => { expect(onSuccess.mostRecentCall.args[0]).toBe( @@ -49,7 +51,7 @@ describe('GitRepositoryAsync', () => { }) }) - fdescribe('buffer events', () => { + describe('buffer events', () => { beforeEach(() => { // This is sync, should be fine in a beforeEach atom.project.setPaths([copyRepository()]) From d5181361a0509ac59afa6f4c6e9479e7a8953f1c Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 6 Nov 2015 21:39:23 -0500 Subject: [PATCH 054/254] Async/await a lot of things. --- spec/git-repository-async-spec.coffee | 8 +- spec/git-repository-async-spec.js | 379 ++++++++++++++++++++++---- 2 files changed, 337 insertions(+), 50 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 03c0e624b..b8df9b364 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -215,7 +215,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - describe ".checkoutHeadForEditor(editor)", -> + xdescribe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] beforeEach -> @@ -230,7 +230,7 @@ describe "GitRepositoryAsync", -> runs -> editor = atom.workspace.getActiveTextEditor() - it "displays a confirmation dialog by default", -> + xit "displays a confirmation dialog by default", -> spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() atom.config.set('editor.confirmCheckoutHeadRevision', true) @@ -239,7 +239,7 @@ describe "GitRepositoryAsync", -> runs -> expect(fs.readFileSync(filePath, 'utf8')).toBe '' - it "does not display a dialog when confirmation is disabled", -> + xit "does not display a dialog when confirmation is disabled", -> spyOn(atom, 'confirm') atom.config.set('editor.confirmCheckoutHeadRevision', false) @@ -393,7 +393,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - it "emits a status-changed event when a buffer's path changes", -> + fit "emits a status-changed event when a buffer's path changes", -> waitsForPromise -> atom.workspace.open('other.txt').then (o) -> editor = o diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2a34a83b8..d602d3e06 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -3,6 +3,8 @@ const fs = require('fs-plus') const path = require('path') const temp = require('temp') +const Git = require('nodegit') +const CompositeDisposable = require('event-kit').CompositeDisposable temp.track() @@ -19,8 +21,41 @@ const copyRepository = () => { return fs.realpathSync(workingDirPath) } +async function waitBetter(fn) { + const p = new Promise() + let first = true + const check = () => { + if (fn()) { + p.resolve() + first = false + return true + } else if (first) { + first = false + return false + } else { + p.reject() + return false + } + } + + if (!check()) { + window.setTimeout(check, 500) + } + return p +} + +fdescribe('GitRepositoryAsync-js', () => { + let subscriptions + + beforeEach(() => { + jasmine.useRealClock() + subscriptions = new CompositeDisposable() + }) + + afterEach(() => { + subscriptions.dispose() + }) -fdescribe('GitRepositoryAsync', () => { describe('@open(path)', () => { it('repo is null when no repository is found', () => { let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) @@ -51,70 +86,322 @@ fdescribe('GitRepositoryAsync', () => { }) }) + describe('.isPathIgnored(path)', () => { + it('resolves true for an ignored path', () => { + let repo = openFixture('ignore.git') + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathIgnored('a.txt').then(onSuccess).catch(e => console.log(e))) + + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + }) + + it('resolves false for a non-ignored path', () => { + let repo = openFixture('ignore.git') + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathIgnored('b.txt').then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + }) + + describe('.isPathModified(path)', () => { + let repo, filePath, newPath, emptyPath + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + emptyPath = path.join(workingDirPath, 'empty-path.txt') + }) + + describe('when the path is unstaged', () => { + it('resolves false if the path has not been modified', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + + it('resolves true if the path is modified', () => { + fs.writeFileSync(filePath, "change") + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + }) + + it('resolves false if the path is new', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(newPath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + + it('resolves false if the path is invalid', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(emptyPath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + }) + }) + + describe('.isPathNew(path)', () => { + let filePath, newPath, repo + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(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', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathNew(newPath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + }) + + it("returns false if the path isn't new", async () => { + let onSuccess = jasmine.createSpy('onSuccess') + + let modified = await repo.isPathModified(newPath).then(onSuccess) + expect(modified).toBeFalsy() + }) + }) + }) + + describe('.checkoutHead(path)', () => { + let filePath, repo + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + }) + + it('no longer reports a path as modified after checkout', async () => { + let modified = await repo.isPathModified(filePath) + expect(modified).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + + modified = await repo.isPathModified(filePath) + expect(modified).toBeTruthy() + + // Don't need to assert that this succeded because waitsForPromise should + // fail if it was rejected.. + await repo.checkoutHead(filePath) + + modified = await repo.isPathModified(filePath) + expect(modified).toBeFalsy() + }) + + it('restores the contents of the path to the original text', async () => { + fs.writeFileSync(filePath, 'ch ch changes') + await repo.checkoutHead(filePath) + xxpect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('fires a did-change-status event if the checkout completes successfully', async () => { + fs.writeFileSync(filePath, 'ch ch changes') + + await repo.getPathStatus(filePath) + + let statusHandler = jasmine.createSpy('statusHandler') + subscriptions.add(repo.onDidChangeStatus(statusHandler)) + + await repo.checkoutHead(filePath) + + expect(statusHandler.callCount).toBe(1) + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) + + await repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + xdescribe('.checkoutHeadForEditor(editor)', () => { + let filePath, editor, repo + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise(() => atom.workspace.open(filePath)) + runs(() => editor = atom.workspace.getActiveTextEditor()) + }) + + xit('displays a confirmation dialog by default', () => { + spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + waitsForPromise(() => repo.checkoutHeadForEditor(editor)) + runs(() => expect(fs.readFileSync(filePath, 'utf8')).toBe('')) + }) + + xit('does not display a dialog when confirmation is disabled', () => { + spyOn(atom, 'confirm') + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + waitsForPromise(() => repo.checkoutHeadForEditor(editor)) + runs(() => { + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + }) + + describe('.getPathStatus(path)', () => { + let filePath, repo + + beforeEach(() => { + let workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + }) + + it('trigger a status-changed event when the new status differs from the last cached one', async () => { + let statusHandler = jasmine.createSpy("statusHandler") + subscriptions.add(repo.onDidChangeStatus(statusHandler)) + fs.writeFileSync(filePath, '') + + await repo.getPathStatus(filePath) + + expect(statusHandler.callCount).toBe(1) + let status = Git.Status.STATUS.WT_MODIFIED + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) + fs.writeFileSync(filePath, 'abc') + + await repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + describe('.getDirectoryStatus(path)', () => { + let directoryPath, filePath, repo + + beforeEach(() => { + let workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + directoryPath = path.join(workingDirectory, 'dir') + filePath = path.join(directoryPath, 'b.txt') + }) + + it('gets the status based on the files inside the directory', async () => { + let result = await repo.getDirectoryStatus(directoryPath) + expect(repo.isStatusModified(result)).toBe(false) + + fs.writeFileSync(filePath, 'abc') + + await repo.getPathStatus(filePath) + + result = await repo.getDirectoryStatus(directoryPath) + expect(repo.isStatusModified(result)).toBe(true) + }) + }) + + describe('.refreshStatus()', () => { + let newPath, modifiedPath, cleanPath, originalModifiedPathText, repo + + beforeEach(() => { + let workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + 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) // specs could be running under symbol path. + }) + + it('returns status information for all new and modified files', async () => { + fs.writeFileSync(modifiedPath, 'making this path modified') + await repo.refreshStatus() + + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + }) + describe('buffer events', () => { beforeEach(() => { // This is sync, should be fine in a beforeEach atom.project.setPaths([copyRepository()]) }) - it('emits a status-changed events when a buffer is saved', () => { - let editor, called - waitsForPromise(function () { - return atom.workspace.open('other.txt').then((o) => { - editor = o - }) - }) + it('emits a status-changed events when a buffer is saved', async () => { + let editor = await atom.workspace.open('other.txt') - runs(() => { - editor.insertNewline() - let repo = atom.project.getRepositories()[0] - repo.async.onDidChangeStatus((c) => { - called = c - }) - editor.save() - }) + editor.insertNewline() - waitsFor(() => { - return Boolean(called) - }) + let repository = atom.project.getRepositories()[0].async + let called + subscriptions.add(repository.onDidChangeStatus(c => called = c)) + editor.save() - runs(() => { - expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) - }) + await waitBetter(() => Boolean(called)) + expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) - it('emits a status-changed event when a buffer is reloaded', () => { - let editor + it('emits a status-changed event when a buffer is reloaded', async () => { let statusHandler = jasmine.createSpy('statusHandler') let reloadHandler = jasmine.createSpy('reloadHandler') - waitsForPromise(function () { - return atom.workspace.open('other.txt').then((o) => { - editor = o - }) - }) + let editor = await atom.workspace.open('other.txt') - runs(() => { - fs.writeFileSync(editor.getPath(), 'changed') - atom.project.getRepositories()[0].async.onDidChangeStatus(statusHandler) - editor.getBuffer().reload() - }) + fs.writeFileSync(editor.getPath(), 'changed') - waitsFor(() => { - return statusHandler.callCount > 0 - }) + let repository = atom.project.getRepositories()[0].async + subscriptions.add(repository.onDidChangeStatus(statusHandler)) + editor.getBuffer().reload() - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - let buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() - }) + await waitBetter(() => statusHandler.callCount > 0) - waitsFor(() => { return reloadHandler.callCount > 0 }) - runs(() => { expect(statusHandler.callCount).toBe(1) }) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + + let buffer = editor.getBuffer() + subscriptions.add(buffer.onDidReload(reloadHandler)) + buffer.reload() + + await waitBetter(() => reloadHandler.callCount > 0) + + expect(statusHandler.callCount).toBe(1) }) + + it("emits a status-changed event when a buffer's path changes", async () => { + let editor = await atom.workspace.open('other.txt') + + fs.writeFileSync(editor.getPath(), 'changed') + + let statusHandler = jasmine.createSpy('statusHandler') + let repository = atom.project.getRepositories()[0].async + subscriptions.add(repository.onDidChangeStatus(statusHandler)) + editor.getBuffer().emitter.emit('did-change-path') + await waitBetter(() => statusHandler.callCount >= 1) + + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + + let pathHandler = jasmine.createSpy('pathHandler') + let buffer = editor.getBuffer() + subscriptions.add(buffer.onDidChangePath(pathHandler)) + buffer.emitter.emit('did-change-path') + await waitBetter(() => pathHandler.callCount >= 1) + expect(statusHandler.callCount).toBe(1) + }) + + // it('stops listening to the buffer when the repository is destroyed (regression)', () => { + // waitsForPromise(() => { + // atom.workspace.open('other.txt').then(o => editor = o) + // }) + // runs(() => { + // atom.project.getRepositories()[0].destroy() + // expect(-> editor.save()).not.toThrow() + // }) + // }) }) }) From c076a047e4b021aec2bc6337b7bab896819816d5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Sat, 7 Nov 2015 01:31:04 -0500 Subject: [PATCH 055/254] Async harder. --- spec/git-repository-async-spec.coffee | 2 +- spec/git-repository-async-spec.js | 180 +++++++++++--------------- 2 files changed, 80 insertions(+), 102 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index b8df9b364..4c25a3ec4 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -393,7 +393,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - fit "emits a status-changed event when a buffer's path changes", -> + it "emits a status-changed event when a buffer's path changes", -> waitsForPromise -> atom.workspace.open('other.txt').then (o) -> editor = o diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d602d3e06..e197a1369 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -21,90 +21,69 @@ const copyRepository = () => { return fs.realpathSync(workingDirPath) } -async function waitBetter(fn) { +async function terribleWait(fn) { const p = new Promise() - let first = true - const check = () => { - if (fn()) { - p.resolve() - first = false - return true - } else if (first) { - first = false - return false - } else { - p.reject() - return false - } - } + waitsFor(fn) + runs(() => p.resolve()) +} - if (!check()) { - window.setTimeout(check, 500) - } - return p +// Git uses heuristics to avoid having to hash a file to tell if it changed. One +// of those is mtime. So our tests are running Super Fast, we could end up +// changing a file multiple times within the same mtime tick, which could lead +// git to think the file didn't change at all. So sometimes we'll need to sleep. +function terribleSleep() { + for (let _ of range(1, 1000)) { ; } } fdescribe('GitRepositoryAsync-js', () => { - let subscriptions - - beforeEach(() => { - jasmine.useRealClock() - subscriptions = new CompositeDisposable() - }) + let repo afterEach(() => { - subscriptions.dispose() + if (repo != null) repo.destroy() }) describe('@open(path)', () => { - it('repo is null when no repository is found', () => { - let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + it('repo is null when no repository is found', async () => { + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - waitsForPromise({shouldReject: true}, () => { - return repo.repoPromise - }) + let threw = false + try { + await repo.repoPromise + } catch (e) { + threw = true + } - runs(() => { - expect(repo.repo).toBe(null) - }) + expect(threw).toBeTruthy() + expect(repo.repo).toBe(null) }) }) describe('.getPath()', () => { xit('returns the repository path for a .git directory path') - it('returns the repository path for a repository path', () => { - let repo = openFixture('master.git') - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.getPath().then(onSuccess)) - - runs(() => { - expect(onSuccess.mostRecentCall.args[0]).toBe( - path.join(__dirname, 'fixtures', 'git', 'master.git') - ) - }) + it('returns the repository path for a repository path', async () => { + repo = openFixture('master.git') + let path = await repo.getPath() + expect(path).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { - it('resolves true for an ignored path', () => { - let repo = openFixture('ignore.git') - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathIgnored('a.txt').then(onSuccess).catch(e => console.log(e))) - - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + it('resolves true for an ignored path', async () => { + repo = openFixture('ignore.git') + let ignored = await repo.isPathIgnored('a.txt') + expect(ignored).toBeTruthy() }) - it('resolves false for a non-ignored path', () => { - let repo = openFixture('ignore.git') - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathIgnored('b.txt').then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false for a non-ignored path', async () => { + repo = openFixture('ignore.git') + let ignored = await repo.isPathIgnored('b.txt') + expect(ignored).toBeFalsy() }) }) describe('.isPathModified(path)', () => { - let repo, filePath, newPath, emptyPath + let filePath, newPath, emptyPath beforeEach(() => { let workingDirPath = copyRepository() @@ -116,35 +95,31 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('resolves false if the path has not been modified', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false if the path has not been modified', async () => { + let modified = await repo.isPathModified(filePath) + expect(modified).toBeFalsy() }) - it('resolves true if the path is modified', () => { + it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, "change") - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + let modified = await repo.isPathModified(filePath) + expect(modified).toBeTruthy() }) - it('resolves false if the path is new', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(newPath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false if the path is new', async () => { + let modified = await repo.isPathModified(newPath) + expect(modified).toBeFalsy() }) - it('resolves false if the path is invalid', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(emptyPath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false if the path is invalid', async () => { + let modified = await repo.isPathModified(emptyPath) + expect(modified).toBeFalsy() }) }) }) describe('.isPathNew(path)', () => { - let filePath, newPath, repo + let filePath, newPath beforeEach(() => { let workingDirPath = copyRepository() @@ -155,23 +130,20 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('returns true if the path is new', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathNew(newPath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + it('returns true if the path is new', async () => { + let isNew = await repo.isPathNew(newPath) + expect(isNew).toBeTruthy() }) it("returns false if the path isn't new", async () => { - let onSuccess = jasmine.createSpy('onSuccess') - - let modified = await repo.isPathModified(newPath).then(onSuccess) + let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) }) }) describe('.checkoutHead(path)', () => { - let filePath, repo + let filePath beforeEach(() => { let workingDirPath = copyRepository() @@ -182,14 +154,15 @@ fdescribe('GitRepositoryAsync-js', () => { it('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() - // Don't need to assert that this succeded because waitsForPromise should - // fail if it was rejected.. await repo.checkoutHead(filePath) + terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() @@ -207,10 +180,11 @@ fdescribe('GitRepositoryAsync-js', () => { await repo.getPathStatus(filePath) let statusHandler = jasmine.createSpy('statusHandler') - subscriptions.add(repo.onDidChangeStatus(statusHandler)) + repo.onDidChangeStatus(statusHandler) await repo.checkoutHead(filePath) + await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) @@ -220,7 +194,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) xdescribe('.checkoutHeadForEditor(editor)', () => { - let filePath, editor, repo + let filePath, editor beforeEach(() => { let workingDirPath = copyRepository() @@ -253,7 +227,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('.getPathStatus(path)', () => { - let filePath, repo + let filePath beforeEach(() => { let workingDirectory = copyRepository() @@ -263,11 +237,13 @@ fdescribe('GitRepositoryAsync-js', () => { it('trigger a status-changed event when the new status differs from the last cached one', async () => { let statusHandler = jasmine.createSpy("statusHandler") - subscriptions.add(repo.onDidChangeStatus(statusHandler)) + repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') await repo.getPathStatus(filePath) + await terribleWait(() => statusHandler.callCount > 0) + expect(statusHandler.callCount).toBe(1) let status = Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) @@ -279,7 +255,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('.getDirectoryStatus(path)', () => { - let directoryPath, filePath, repo + let directoryPath, filePath beforeEach(() => { let workingDirectory = copyRepository() @@ -289,12 +265,14 @@ fdescribe('GitRepositoryAsync-js', () => { }) it('gets the status based on the files inside the directory', async () => { + await repo.checkoutHead(filePath) + terribleSleep() + let result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(false) fs.writeFileSync(filePath, 'abc') - - await repo.getPathStatus(filePath) + terribleSleep() result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(true) @@ -302,7 +280,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('.refreshStatus()', () => { - let newPath, modifiedPath, cleanPath, originalModifiedPathText, repo + let newPath, modifiedPath, cleanPath, originalModifiedPathText beforeEach(() => { let workingDirectory = copyRepository() @@ -338,10 +316,10 @@ fdescribe('GitRepositoryAsync-js', () => { let repository = atom.project.getRepositories()[0].async let called - subscriptions.add(repository.onDidChangeStatus(c => called = c)) + repository.onDidChangeStatus(c => called = c) editor.save() - await waitBetter(() => Boolean(called)) + await terribleWait(() => Boolean(called)) expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) @@ -354,19 +332,19 @@ fdescribe('GitRepositoryAsync-js', () => { fs.writeFileSync(editor.getPath(), 'changed') let repository = atom.project.getRepositories()[0].async - subscriptions.add(repository.onDidChangeStatus(statusHandler)) + repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() - await waitBetter(() => statusHandler.callCount > 0) + await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) let buffer = editor.getBuffer() - subscriptions.add(buffer.onDidReload(reloadHandler)) + buffer.onDidReload(reloadHandler) buffer.reload() - await waitBetter(() => reloadHandler.callCount > 0) + await terribleWait(() => reloadHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) @@ -378,18 +356,18 @@ fdescribe('GitRepositoryAsync-js', () => { let statusHandler = jasmine.createSpy('statusHandler') let repository = atom.project.getRepositories()[0].async - subscriptions.add(repository.onDidChangeStatus(statusHandler)) + repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') - await waitBetter(() => statusHandler.callCount >= 1) + await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) let pathHandler = jasmine.createSpy('pathHandler') let buffer = editor.getBuffer() - subscriptions.add(buffer.onDidChangePath(pathHandler)) + buffer.onDidChangePath(pathHandler) buffer.emitter.emit('did-change-path') - await waitBetter(() => pathHandler.callCount >= 1) + await terribleWait(() => pathHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) From 87da3579e8a0ac8f831660b89eeeeb7d1ebc98ed Mon Sep 17 00:00:00 2001 From: joshaber Date: Sat, 7 Nov 2015 01:35:57 -0500 Subject: [PATCH 056/254] Cite it. --- spec/git-repository-async-spec.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e197a1369..094139305 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -27,10 +27,11 @@ async function terribleWait(fn) { runs(() => p.resolve()) } -// Git uses heuristics to avoid having to hash a file to tell if it changed. One -// of those is mtime. So our tests are running Super Fast, we could end up -// changing a file multiple times within the same mtime tick, which could lead -// git to think the file didn't change at all. So sometimes we'll need to sleep. +// Git uses heuristics to avoid having to needlessly hash a file to tell if it +// changed. One of those is mtime. If our tests are running Super Fast, we could +// end up changing a file multiple times within the same mtime tick, which could +// lead git to think the file didn't change at all. So sometimes we'll need to +// sleep. (https://www.kernel.org/pub/software/scm/git/docs/technical/racy-git.txt) function terribleSleep() { for (let _ of range(1, 1000)) { ; } } From 995d912d760d54401a001edb419c6031a7e21192 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 13:35:31 +0100 Subject: [PATCH 057/254] remove devDependencies due to license issues --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index b55d24502..74805410b 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,6 @@ "underscore-plus": "^1.6.6", "yargs": "^3.23.0" }, - "devDependencies" : { - "babel-eslint": "^4.1.3", - "standard": "^5.3.1" - }, "packageDependencies": { "atom-dark-syntax": "0.27.0", "atom-dark-ui": "0.51.0", From 60f9180ec36550f0b2e8d00dcd606bf7733b8804 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 14:26:24 +0100 Subject: [PATCH 058/254] add license overrides for shelljs and log-driver --- build/tasks/license-overrides.coffee | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index 287d44b63..e716f76a2 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -82,3 +82,44 @@ module.exports = 'core-js@0.4.10': license: 'MIT' source: 'http://rock.mit-license.org linked in source files and bower.json says MIT' + 'log-driver@1.2.4': + license: 'ISC' + source: 'LICENSE file in the repository' + sourceText: """ + Copyright (c) 2014, Gregg Caines, gregg@caines.ca + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + """ + 'shelljs@0.3.0': + license: 'BSD' + source: 'LICENSE file in repository - 3-clause BSD (aka BSD-new)' + sourceText: """ + Copyright (c) 2012, Artur Adib + All rights reserved. + + You may use this project under the terms of the New BSD license as follows: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Artur Adib nor the + names of the contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + """ From 7598be861bb6531599a50d8fe4ee0e9d53ac7ca3 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 15:08:03 +0100 Subject: [PATCH 059/254] Add empty ignore array to standard config - This version of grunt-standard requires SOMETHING to be set here, or it dies hard because it tries to push undefined into the array of ignored paths --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 74805410b..33f0fcc8b 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "test": "node script/test" }, "standard": { + "ignore": [], "parser": "babel-eslint", "globals": [ "atom", From 93046f550f56011130b4071dec54ce4723f8d282 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 15:13:00 +0100 Subject: [PATCH 060/254] Clean up lint errors from `./script/grunt standard:src` --- src/git-repository-async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a88ee6e6e..48fedc888 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -2,7 +2,7 @@ const Git = require('nodegit') const path = require('path') -const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const {Emitter, CompositeDisposable} = require('event-kit') const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -211,7 +211,6 @@ module.exports = class GitRepositoryAsync { }) ) - this.subscriptions.add(bufferSubscriptions) return } From ff4f26e1ec7a8863d1be2ff0fdaf1c68135280ec Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 15:38:24 +0100 Subject: [PATCH 061/254] coffeelint fix --- spec/git-repository-async-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 4c25a3ec4..c6402d1fb 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -16,7 +16,7 @@ copyRepository = -> fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) fs.realpathSync(workingDirPath) -openFixture = (fixture)-> +openFixture = (fixture) -> GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) describe "GitRepositoryAsync", -> From 14d470a5e5e554b6c39c16653bb7f1db436c7c5a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:01:35 +0100 Subject: [PATCH 062/254] unforce spec --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 094139305..a8c35b60a 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -36,7 +36,7 @@ function terribleSleep() { for (let _ of range(1, 1000)) { ; } } -fdescribe('GitRepositoryAsync-js', () => { +describe('GitRepositoryAsync-js', () => { let repo afterEach(() => { From bdc8a38045f75da7b8491dfff854db8fe722ebf2 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:15:55 +0100 Subject: [PATCH 063/254] add final buffer event spec --- spec/git-repository-async-spec.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a8c35b60a..e104be5e2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -372,15 +372,16 @@ describe('GitRepositoryAsync-js', () => { expect(statusHandler.callCount).toBe(1) }) - // it('stops listening to the buffer when the repository is destroyed (regression)', () => { - // waitsForPromise(() => { - // atom.workspace.open('other.txt').then(o => editor = o) - // }) - // runs(() => { - // atom.project.getRepositories()[0].destroy() - // expect(-> editor.save()).not.toThrow() - // }) - // }) + it('stops listening to the buffer when the repository is destroyed (regression)', function () { + let editor + waitsForPromise(async function () { + editor = await atom.workspace.open('other.txt') + }) + runs(function () { + atom.project.getRepositories()[0].destroy() + expect(function () { editor.save() }).not.toThrow() + }) + }) }) }) From a9dae48ac4a64eb075d2eb0ebd47201fe7dafcf2 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 10:39:02 -0500 Subject: [PATCH 064/254] It's better if we actually run the tests. --- spec/git-repository-async-spec.js | 117 ++++++++++++++---------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a8c35b60a..6456185a6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -10,11 +10,11 @@ temp.track() const GitRepositoryAsync = require('../src/git-repository-async') -const openFixture = (fixture) => { +function openFixture(fixture) { return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } -const copyRepository = () => { +function copyRepository() { let workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) @@ -23,20 +23,17 @@ const copyRepository = () => { async function terribleWait(fn) { const p = new Promise() - waitsFor(fn) - runs(() => p.resolve()) + p.resolve() + expect(fn()).toBeTruthy() } -// Git uses heuristics to avoid having to needlessly hash a file to tell if it -// changed. One of those is mtime. If our tests are running Super Fast, we could -// end up changing a file multiple times within the same mtime tick, which could -// lead git to think the file didn't change at all. So sometimes we'll need to -// sleep. (https://www.kernel.org/pub/software/scm/git/docs/technical/racy-git.txt) -function terribleSleep() { - for (let _ of range(1, 1000)) { ; } +function asyncIt(name, fn) { + it(name, () => { + waitsForPromise(fn) + }) } -describe('GitRepositoryAsync-js', () => { +fdescribe('GitRepositoryAsync-js', () => { let repo afterEach(() => { @@ -44,39 +41,41 @@ describe('GitRepositoryAsync-js', () => { }) describe('@open(path)', () => { - it('repo is null when no repository is found', async () => { - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + it('repo is null when no repository is found', () => { + waitsForPromise(async () => { + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - let threw = false - try { - await repo.repoPromise - } catch (e) { - threw = true - } + let threw = false + try { + await repo.repoPromise + } catch(e) { + threw = true + } - expect(threw).toBeTruthy() - expect(repo.repo).toBe(null) + expect(threw).toBeTruthy() + expect(repo.repo).toBe(null) + }) }) }) describe('.getPath()', () => { xit('returns the repository path for a .git directory path') - it('returns the repository path for a repository path', async () => { + asyncIt('returns the repository path for a repository path', async () => { repo = openFixture('master.git') - let path = await repo.getPath() - expect(path).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + let repoPath = await repo.getPath() + expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { - it('resolves true for an ignored path', async () => { + asyncIt('resolves true for an ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('a.txt') expect(ignored).toBeTruthy() }) - it('resolves false for a non-ignored path', async () => { + asyncIt('resolves false for a non-ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('b.txt') expect(ignored).toBeFalsy() @@ -96,23 +95,23 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('resolves false if the path has not been modified', async () => { + asyncIt('resolves false if the path has not been modified', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) - it('resolves true if the path is modified', async () => { + asyncIt('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, "change") let modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) - it('resolves false if the path is new', async () => { + asyncIt('resolves false if the path is new', async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) - it('resolves false if the path is invalid', async () => { + asyncIt('resolves false if the path is invalid', async () => { let modified = await repo.isPathModified(emptyPath) expect(modified).toBeFalsy() }) @@ -131,12 +130,12 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('returns true if the path is new', async () => { + asyncIt('returns true if the path is new', async () => { let isNew = await repo.isPathNew(newPath) expect(isNew).toBeTruthy() }) - it("returns false if the path isn't new", async () => { + asyncIt("returns false if the path isn't new", async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) @@ -152,30 +151,28 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirPath, 'a.txt') }) - it('no longer reports a path as modified after checkout', async () => { + asyncIt('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() fs.writeFileSync(filePath, 'ch ch changes') - terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() await repo.checkoutHead(filePath) - terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) - it('restores the contents of the path to the original text', async () => { + asyncIt('restores the contents of the path to the original text', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.checkoutHead(filePath) - xxpect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(fs.readFileSync(filePath, 'utf8')).toBe('') }) - it('fires a did-change-status event if the checkout completes successfully', async () => { + asyncIt('fires a did-change-status event if the checkout completes successfully', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.getPathStatus(filePath) @@ -185,7 +182,7 @@ describe('GitRepositoryAsync-js', () => { await repo.checkoutHead(filePath) - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) @@ -236,14 +233,14 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirectory, 'file.txt') }) - it('trigger a status-changed event when the new status differs from the last cached one', async () => { + asyncIt('trigger a status-changed event when the new status differs from the last cached one', async () => { let statusHandler = jasmine.createSpy("statusHandler") repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') await repo.getPathStatus(filePath) - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) let status = Git.Status.STATUS.WT_MODIFIED @@ -265,15 +262,13 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(directoryPath, 'b.txt') }) - it('gets the status based on the files inside the directory', async () => { + asyncIt('gets the status based on the files inside the directory', async () => { await repo.checkoutHead(filePath) - terribleSleep() let result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(false) fs.writeFileSync(filePath, 'abc') - terribleSleep() result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(true) @@ -294,7 +289,7 @@ describe('GitRepositoryAsync-js', () => { newPath = fs.absolute(newPath) // specs could be running under symbol path. }) - it('returns status information for all new and modified files', async () => { + asyncIt('returns status information for all new and modified files', async () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() @@ -310,7 +305,7 @@ describe('GitRepositoryAsync-js', () => { atom.project.setPaths([copyRepository()]) }) - it('emits a status-changed events when a buffer is saved', async () => { + asyncIt('emits a status-changed event when a buffer is saved', async () => { let editor = await atom.workspace.open('other.txt') editor.insertNewline() @@ -320,11 +315,11 @@ describe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(c => called = c) editor.save() - await terribleWait(() => Boolean(called)) + // await terribleWait(() => Boolean(called)) expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) - it('emits a status-changed event when a buffer is reloaded', async () => { + asyncIt('emits a status-changed event when a buffer is reloaded', async () => { let statusHandler = jasmine.createSpy('statusHandler') let reloadHandler = jasmine.createSpy('reloadHandler') @@ -336,7 +331,7 @@ describe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) @@ -345,12 +340,12 @@ describe('GitRepositoryAsync-js', () => { buffer.onDidReload(reloadHandler) buffer.reload() - await terribleWait(() => reloadHandler.callCount > 0) + // await terribleWait(() => reloadHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) - it("emits a status-changed event when a buffer's path changes", async () => { + asyncIt("emits a status-changed event when a buffer's path changes", async () => { let editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') @@ -359,7 +354,7 @@ describe('GitRepositoryAsync-js', () => { let repository = atom.project.getRepositories()[0].async repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) @@ -368,19 +363,15 @@ describe('GitRepositoryAsync-js', () => { let buffer = editor.getBuffer() buffer.onDidChangePath(pathHandler) buffer.emitter.emit('did-change-path') - await terribleWait(() => pathHandler.callCount > 0) + // await terribleWait(() => pathHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) - // it('stops listening to the buffer when the repository is destroyed (regression)', () => { - // waitsForPromise(() => { - // atom.workspace.open('other.txt').then(o => editor = o) - // }) - // runs(() => { - // atom.project.getRepositories()[0].destroy() - // expect(-> editor.save()).not.toThrow() - // }) - // }) + asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + let editor = await atom.workspace.open('other.txt') + atom.project.getRepositories()[0].destroy() + expect(() => editor.save()).not.toThrow() + }) }) }) From e3af723c9b1fc093a65785835396576119a30ac7 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:40:44 +0100 Subject: [PATCH 065/254] Copy relativize and getPathStatus from #9469 --- spec/git-repository-async-spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e104be5e2..ee1ec830b 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -384,4 +384,6 @@ describe('GitRepositoryAsync-js', () => { }) }) + xdescribe('GitRepositoryAsync::relativize(filePath)') + }) From 0322d8d3dada0334edba74768ce4b54725ca7757 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:48:02 +0100 Subject: [PATCH 066/254] Use our own ::relativize This allows us to remove the git-utils import. --- src/git-repository-async.js | 105 +++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 48fedc888..532c89292 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,11 +9,6 @@ const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -// Temporary requires -// ================== -// GitUtils is temporarily used for ::relativize only, because I don't want -// to port it just yet. TODO: remove -const GitUtils = require('git-utils') // Just using this for _.isEqual and _.object, we should impl our own here const _ = require('underscore-plus') @@ -32,7 +27,6 @@ module.exports = class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) let {project, refreshOnWindowFocus} = options @@ -96,7 +90,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }).then(() => { @@ -120,20 +114,32 @@ module.exports = class GitRepositoryAsync { }) } + // Returns a Promise that resolves to the status bit of a given path if it has + // one, otherwise 'current'. // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - let relativePath = this._gitUtilsRepo.relativize(_path) + let relativePath return this.repoPromise.then((repo) => { + relativePath = this.relativize(_path, repo.workdir()) return this._filterStatusesByPath(_path) - }).then((statuses) => { + }).then(statuses => { let cachedStatus = this.pathStatusCache[relativePath] || 0 let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + + if (status > 0) { + this.pathStatusCache[relativePath] = status + } else { + delete this.pathStatusCache[relativePath] + } + if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } - this.pathStatusCache[relativePath] = status + return status + }).catch(e => { + console.trace(e) }) } @@ -145,10 +151,9 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { - let relativePath = this._gitUtilsRepo.relativize(directoryPath) // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { - return this._filterStatusesByDirectory(relativePath) + return this._filterStatusesByDirectory(this.relativize(directoryPath, repo.workdir())) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { let directoryStatus = 0 @@ -189,6 +194,78 @@ module.exports = class GitRepositoryAsync { // Section: Private // ================ + relativizeAsync (_path) { + this.repoPromise.then((repo) => { + return this.relativize(_path, repo.workdir()) + }) + } + + relativize (_path, workingDirectory) { + if (!Boolean(workingDirectory)) { + workingDirectory = this.getWorkingDirectorySync() // TODO + } + // Cargo-culted from git-utils. Could use a refactor maybe. Short circuits everywhere! + if (!_path) { + return _path + } + + if (process.platform === 'win32') { + _path = _path.replace(/\\/g, '/') + } else { + if (_path[0] !== '/') { + return _path + } + } + + if (this.isCaseInsensitive) { + let lowerCasePath = _path.toLowerCase() + + if (workingDirectory) { + workingDirectory = workingDirectory.toLowerCase() + if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { + return _path.substring(workingDirectory.length + 1) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + + if (this.openedWorkingDirectory) { + workingDirectory = this.openedWorkingDirectory.toLowerCase() + if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { + return _path.substring(workingDirectory.length + 1) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + } else { + workingDirectory = this.getWorkingDirectory() // TODO + if (workingDirectory) { + if (_path.indexOf(`${workingDirectory}/`) === 0) { + return _path.substring(workingDirectory.length + 1) + } else { + if (_path === workingDirectory) { + return '' + } + } + } + + if (this.openedWorkingDirectory) { + if (_path.indexOf(`${this.openedWorkingDirectory}/`) === 0) { + return _path.substring(this.openedWorkingDirectory.length + 1) + } else { + if (_path === this.openedWorkingDirectory) { + return '' + } + } + } + } + return _path + } + subscribeToBuffer (buffer) { let bufferSubscriptions = new CompositeDisposable() @@ -216,7 +293,9 @@ module.exports = class GitRepositoryAsync { } getCachedPathStatus (_path) { - return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] + return this.repoPromise.then((repo) => { + return this.pathStatusCache[this.relativize(_path, repo.workdir())] + }) } isStatusNew (statusBit) { From d8985c8175f25e6f40026c07980846c68d4da935 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:51:02 +0100 Subject: [PATCH 067/254] await getCachedPathStatus in specs --- spec/git-repository-async-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 7543430e7..f8a12fb4f 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -293,9 +293,9 @@ fdescribe('GitRepositoryAsync-js', () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) }) From 8f59693d839f11f8f792c4271648d9e83d4541ba Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 10:41:17 -0500 Subject: [PATCH 068/254] Use asyncIt here too. --- spec/git-repository-async-spec.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index f8a12fb4f..44cc4fd0b 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -41,20 +41,18 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('@open(path)', () => { - it('repo is null when no repository is found', () => { - waitsForPromise(async () => { - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + asyncIt('repo is null when no repository is found', async () => { + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - let threw = false - try { - await repo.repoPromise - } catch(e) { - threw = true - } + let threw = false + try { + await repo.repoPromise + } catch(e) { + threw = true + } - expect(threw).toBeTruthy() - expect(repo.repo).toBe(null) - }) + expect(threw).toBeTruthy() + expect(repo.repo).toBe(null) }) }) From cbc3dfb555d4b73901676f5610eb7733fd5fccf3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 10:51:28 -0500 Subject: [PATCH 069/254] :fire: terribleWait. --- spec/git-repository-async-spec.js | 61 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 44cc4fd0b..cc2fe42d9 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -21,18 +21,18 @@ function copyRepository() { return fs.realpathSync(workingDirPath) } -async function terribleWait(fn) { - const p = new Promise() - p.resolve() - expect(fn()).toBeTruthy() -} - function asyncIt(name, fn) { it(name, () => { waitsForPromise(fn) }) } +function xasyncIt(name, fn) { + xit(name, () => { + waitsForPromise(fn) + }) +} + fdescribe('GitRepositoryAsync-js', () => { let repo @@ -180,7 +180,6 @@ fdescribe('GitRepositoryAsync-js', () => { await repo.checkoutHead(filePath) - // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) @@ -238,8 +237,6 @@ fdescribe('GitRepositoryAsync-js', () => { await repo.getPathStatus(filePath) - // await terribleWait(() => statusHandler.callCount > 0) - expect(statusHandler.callCount).toBe(1) let status = Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) @@ -313,8 +310,8 @@ fdescribe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(c => called = c) editor.save() - // await terribleWait(() => Boolean(called)) - expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) + waitsFor(() => Boolean(called)) + runs(() => expect(called).toEqual({path: editor.getPath(), pathStatus: 256})) }) asyncIt('emits a status-changed event when a buffer is reloaded', async () => { @@ -329,18 +326,18 @@ fdescribe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() - // await terribleWait(() => statusHandler.callCount > 0) + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + let buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() - let buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() - - // await terribleWait(() => reloadHandler.callCount > 0) - - expect(statusHandler.callCount).toBe(1) + waitsFor(() => reloadHandler.callCount > 0) + runs(() => expect(statusHandler.callCount).toBe(1)) + }) }) asyncIt("emits a status-changed event when a buffer's path changes", async () => { @@ -352,20 +349,22 @@ fdescribe('GitRepositoryAsync-js', () => { let repository = atom.project.getRepositories()[0].async repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') - // await terribleWait(() => statusHandler.callCount > 0) + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + let pathHandler = jasmine.createSpy('pathHandler') + let buffer = editor.getBuffer() + buffer.onDidChangePath(pathHandler) + buffer.emitter.emit('did-change-path') - let pathHandler = jasmine.createSpy('pathHandler') - let buffer = editor.getBuffer() - buffer.onDidChangePath(pathHandler) - buffer.emitter.emit('did-change-path') - // await terribleWait(() => pathHandler.callCount > 0) - expect(statusHandler.callCount).toBe(1) + waitsFor(() => pathHandler.callCount > 0) + runs(() => expect(statusHandler.callCount).toBe(1)) + }) }) - asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + xasyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { let editor = await atom.workspace.open('other.txt') atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() From 3d2ed074707cc3e8983850e5cdf2a108ddceabf1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:56:51 +0100 Subject: [PATCH 070/254] Revert "Use our own ::relativize" This reverts commit 0322d8d3dada0334edba74768ce4b54725ca7757. --- src/git-repository-async.js | 105 +++++------------------------------- 1 file changed, 13 insertions(+), 92 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 532c89292..48fedc888 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,6 +9,11 @@ const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE +// Temporary requires +// ================== +// GitUtils is temporarily used for ::relativize only, because I don't want +// to port it just yet. TODO: remove +const GitUtils = require('git-utils') // Just using this for _.isEqual and _.object, we should impl our own here const _ = require('underscore-plus') @@ -27,6 +32,7 @@ module.exports = class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} + this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) let {project, refreshOnWindowFocus} = options @@ -90,7 +96,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, repo.workdir())] + checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }).then(() => { @@ -114,32 +120,20 @@ module.exports = class GitRepositoryAsync { }) } - // Returns a Promise that resolves to the status bit of a given path if it has - // one, otherwise 'current'. // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - let relativePath + let relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { - relativePath = this.relativize(_path, repo.workdir()) return this._filterStatusesByPath(_path) - }).then(statuses => { + }).then((statuses) => { let cachedStatus = this.pathStatusCache[relativePath] || 0 let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - - if (status > 0) { - this.pathStatusCache[relativePath] = status - } else { - delete this.pathStatusCache[relativePath] - } - if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } - + this.pathStatusCache[relativePath] = status return status - }).catch(e => { - console.trace(e) }) } @@ -151,9 +145,10 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { + let relativePath = this._gitUtilsRepo.relativize(directoryPath) // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { - return this._filterStatusesByDirectory(this.relativize(directoryPath, repo.workdir())) + return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { let directoryStatus = 0 @@ -194,78 +189,6 @@ module.exports = class GitRepositoryAsync { // Section: Private // ================ - relativizeAsync (_path) { - this.repoPromise.then((repo) => { - return this.relativize(_path, repo.workdir()) - }) - } - - relativize (_path, workingDirectory) { - if (!Boolean(workingDirectory)) { - workingDirectory = this.getWorkingDirectorySync() // TODO - } - // Cargo-culted from git-utils. Could use a refactor maybe. Short circuits everywhere! - if (!_path) { - return _path - } - - if (process.platform === 'win32') { - _path = _path.replace(/\\/g, '/') - } else { - if (_path[0] !== '/') { - return _path - } - } - - if (this.isCaseInsensitive) { - let lowerCasePath = _path.toLowerCase() - - if (workingDirectory) { - workingDirectory = workingDirectory.toLowerCase() - if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { - return _path.substring(workingDirectory.length + 1) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } - } - - if (this.openedWorkingDirectory) { - workingDirectory = this.openedWorkingDirectory.toLowerCase() - if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { - return _path.substring(workingDirectory.length + 1) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } - } - } else { - workingDirectory = this.getWorkingDirectory() // TODO - if (workingDirectory) { - if (_path.indexOf(`${workingDirectory}/`) === 0) { - return _path.substring(workingDirectory.length + 1) - } else { - if (_path === workingDirectory) { - return '' - } - } - } - - if (this.openedWorkingDirectory) { - if (_path.indexOf(`${this.openedWorkingDirectory}/`) === 0) { - return _path.substring(this.openedWorkingDirectory.length + 1) - } else { - if (_path === this.openedWorkingDirectory) { - return '' - } - } - } - } - return _path - } - subscribeToBuffer (buffer) { let bufferSubscriptions = new CompositeDisposable() @@ -293,9 +216,7 @@ module.exports = class GitRepositoryAsync { } getCachedPathStatus (_path) { - return this.repoPromise.then((repo) => { - return this.pathStatusCache[this.relativize(_path, repo.workdir())] - }) + return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] } isStatusNew (statusBit) { From 371c68e65e05843cd4a9ef641184a9eba1f8adb7 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:57:08 +0100 Subject: [PATCH 071/254] Revert "await getCachedPathStatus in specs" This reverts commit d8985c8175f25e6f40026c07980846c68d4da935. --- spec/git-repository-async-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index cc2fe42d9..d02d31dd2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -288,9 +288,9 @@ fdescribe('GitRepositoryAsync-js', () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() - expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) }) From 4b8bad2b0bfc5482fb8389ef10d6615588379100 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:10:49 -0500 Subject: [PATCH 072/254] Focus async its. --- spec/git-repository-async-spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d02d31dd2..2470d24d3 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -27,6 +27,12 @@ function asyncIt(name, fn) { }) } +function fasyncIt(name, fn) { + fit(name, () => { + waitsForPromise(fn) + }) +} + function xasyncIt(name, fn) { xit(name, () => { waitsForPromise(fn) From 9da71cf7c0f23b2dc27590096665b5885b3f1c6f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:14:50 -0500 Subject: [PATCH 073/254] Wait for the first refresh to complete. --- spec/git-repository-async-spec.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2470d24d3..8b02c86c6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -302,8 +302,15 @@ fdescribe('GitRepositoryAsync-js', () => { describe('buffer events', () => { beforeEach(() => { - // This is sync, should be fine in a beforeEach atom.project.setPaths([copyRepository()]) + + // When the path is added to the project, the repository is refreshed. We + // need to wait for that to complete before the tests continue so that + // we're in a known state. + let repository = atom.project.getRepositories()[0].async + let statusHandler = jasmine.createSpy('statusHandler') + repository.onDidChangeStatuses(statusHandler) + waitsFor(() => statusHandler.callCount > 0) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { From 51b15a6c00414da450099252bf61cf8cc3da6544 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:42:12 -0500 Subject: [PATCH 074/254] Cleanup --- spec/git-repository-async-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 8b02c86c6..106d02540 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -4,12 +4,11 @@ const fs = require('fs-plus') const path = require('path') const temp = require('temp') const Git = require('nodegit') -const CompositeDisposable = require('event-kit').CompositeDisposable - -temp.track() const GitRepositoryAsync = require('../src/git-repository-async') +temp.track() + function openFixture(fixture) { return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } @@ -302,7 +301,8 @@ fdescribe('GitRepositoryAsync-js', () => { describe('buffer events', () => { beforeEach(() => { - atom.project.setPaths([copyRepository()]) + const workingDirectory = copyRepository() + atom.project.setPaths([workingDirectory]) // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that From b97a7fb2c395fc8a8e43ad2a6a4a02868ed3f2f6 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:42:27 -0500 Subject: [PATCH 075/254] Unfocus --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 106d02540..a09e4f13a 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -38,7 +38,7 @@ function xasyncIt(name, fn) { }) } -fdescribe('GitRepositoryAsync-js', () => { +describe('GitRepositoryAsync-js', () => { let repo afterEach(() => { From c11f8f77ba081de5369f90d98741afb15d4e7d4b Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 13:41:53 -0500 Subject: [PATCH 076/254] Re-enable that test. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a09e4f13a..e44653b02 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -377,7 +377,7 @@ describe('GitRepositoryAsync-js', () => { }) }) - xasyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { let editor = await atom.workspace.open('other.txt') atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() From f0283df7d5f850ffd7185fff1c48af22ec713269 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 14:24:15 -0500 Subject: [PATCH 077/254] Re-add this test. --- spec/git-repository-async-spec.js | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e44653b02..0e94d360a 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -6,6 +6,7 @@ const temp = require('temp') const Git = require('nodegit') const GitRepositoryAsync = require('../src/git-repository-async') +const Project = require('../src/project') temp.track() @@ -384,6 +385,46 @@ describe('GitRepositoryAsync-js', () => { }) }) + describe('when a project is deserialized', () => { + let project2 + + beforeEach(() => { + atom.project.setPaths([copyRepository()]) + + let repository = atom.project.getRepositories()[0].async + let statusHandler = jasmine.createSpy('statusHandler') + repository.onDidChangeStatuses(statusHandler) + waitsFor(() => statusHandler.callCount > 0) + }) + + afterEach(() => { + if (project2) project2.destroy() + }) + + asyncIt('subscribes to all the serialized buffers in the project', async () => { + await atom.workspace.open('file.txt') + + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + project2.deserialize(atom.project.serialize(), atom.deserializers) + let buffer = project2.getBuffers()[0] + + waitsFor(() => buffer.loaded) + runs(() => { + buffer.append('changes') + + let statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].async.onDidChangeStatus(statusHandler) + buffer.save() + + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + }) + }) + }) + }) + xdescribe('GitRepositoryAsync::relativize(filePath)') }) From 0b378b0f87ca75d5e23ee82acfd97662e6d9514f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 21:11:29 -0500 Subject: [PATCH 078/254] Use node-pre-gyp to grab binaries of nodegit. --- package.json | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 33f0fcc8b..e688fd686 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", + "node-pre-gyp": "^0.6.15", "nodegit": "~0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", @@ -158,18 +159,18 @@ "standard": { "ignore": [], "parser": "babel-eslint", - "globals": [ - "atom", - "afterEach", - "beforeEach", - "describe", - "expect", - "it", - "jasmine", - "runs", - "spyOn", - "waitsFor", - "waitsForPromise" - ] + "globals": [ + "atom", + "afterEach", + "beforeEach", + "describe", + "expect", + "it", + "jasmine", + "runs", + "spyOn", + "waitsFor", + "waitsForPromise" + ] } } From 628e810758765836acc78922c1c72c6cba77ab83 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 10 Nov 2015 15:23:36 -0500 Subject: [PATCH 079/254] Expose the promise for refreshStatus. --- src/git-repository.coffee | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 46d379f98..9ff0771b3 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -470,24 +470,31 @@ class GitRepository # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. + # + # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> - @async.refreshStatus() - @handlerPath ?= require.resolve('./repository-status-handler') + asyncRefresh = @async.refreshStatus() + syncRefresh = new Promise (resolve, reject) => + @handlerPath ?= require.resolve('./repository-status-handler') - @statusTask?.terminate() - @statusTask = Task.once @handlerPath, @getPath(), ({statuses, upstream, branch, submodules}) => - statusesUnchanged = _.isEqual(statuses, @statuses) and - _.isEqual(upstream, @upstream) and - _.isEqual(branch, @branch) and - _.isEqual(submodules, @submodules) + @statusTask?.terminate() + @statusTask = Task.once @handlerPath, @getPath(), ({statuses, upstream, branch, submodules}) => + statusesUnchanged = _.isEqual(statuses, @statuses) and + _.isEqual(upstream, @upstream) and + _.isEqual(branch, @branch) and + _.isEqual(submodules, @submodules) - @statuses = statuses - @upstream = upstream - @branch = branch - @submodules = submodules + @statuses = statuses + @upstream = upstream + @branch = branch + @submodules = submodules - for submodulePath, submoduleRepo of @getRepo().submodules - submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} + for submodulePath, submoduleRepo of @getRepo().submodules + submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} - unless statusesUnchanged - @emitter.emit 'did-change-statuses' + resolve() + + unless statusesUnchanged + @emitter.emit 'did-change-statuses' + + return Promise.all([asyncRefresh, syncRefresh]) From 55cc08215dd08ba053824fa4e68b5f759b3239fb Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 10 Nov 2015 15:23:47 -0500 Subject: [PATCH 080/254] Be even less racy. --- spec/git-repository-async-spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0e94d360a..38ee1b8bc 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -307,11 +307,11 @@ describe('GitRepositoryAsync-js', () => { // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that - // we're in a known state. - let repository = atom.project.getRepositories()[0].async - let statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatuses(statusHandler) - waitsFor(() => statusHandler.callCount > 0) + // we're in a known state. *But* it's really hard to observe that from the + // outside in a non-racy fashion. So let's refresh again and wait for it + // to complete before we continue. + let repository = atom.project.getRepositories()[0] + waitsForPromise(() => repository.refreshStatus()) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { @@ -391,10 +391,10 @@ describe('GitRepositoryAsync-js', () => { beforeEach(() => { atom.project.setPaths([copyRepository()]) - let repository = atom.project.getRepositories()[0].async - let statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatuses(statusHandler) - waitsFor(() => statusHandler.callCount > 0) + // See the comment in the 'buffer events' beforeEach for why we need to do + // this. + let repository = atom.project.getRepositories()[0] + waitsForPromise(() => repository.refreshStatus()) }) afterEach(() => { From 7421568134561b4750666225cd5271fc947f353f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 11:08:48 +0100 Subject: [PATCH 081/254] Add simple ::relativize() - remove spec - Remove GitUtils from async repo - Doesn't cover all the cases as the one in git-utils but I think they might have been specific to that library's implementation --- spec/git-repository-async-spec.js | 35 +++++++++++++++-- src/git-repository-async.js | 62 ++++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0e94d360a..42c071e44 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -294,9 +294,9 @@ describe('GitRepositoryAsync-js', () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) }) @@ -425,6 +425,33 @@ describe('GitRepositoryAsync-js', () => { }) }) - xdescribe('GitRepositoryAsync::relativize(filePath)') + describe('GitRepositoryAsync::relativize(filePath, workdir)', () => { + let repository + + beforeEach(() => { + atom.project.setPaths([copyRepository()]) + repository = atom.project.getRepositories()[0].async + }) + + // This is a change in implementation from the git-utils version + it('just returns path if workdir is not provided', () => { + let _path = '/foo/bar/baz.txt' + let relPath = repository.relativize(_path) + expect(_path).toEqual(relPath) + }) + + it('relativizes a repo path', () => { + let workdir = '/tmp/foo/bar/baz/' + let relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) + expect(relativizedPath).toBe('a/b.txt') + }) + + it("doesn't require workdir to end in a slash", () => { + let workdir = '/tmp/foo/bar/baz' + let relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) + expect(relativizedPath).toBe('a/b.txt') + }) + + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 48fedc888..fa5518d85 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,5 +1,6 @@ 'use babel' +const fs = require('fs-plus') const Git = require('nodegit') const path = require('path') const {Emitter, CompositeDisposable} = require('event-kit') @@ -9,11 +10,6 @@ const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -// Temporary requires -// ================== -// GitUtils is temporarily used for ::relativize only, because I don't want -// to port it just yet. TODO: remove -const GitUtils = require('git-utils') // Just using this for _.isEqual and _.object, we should impl our own here const _ = require('underscore-plus') @@ -32,8 +28,8 @@ module.exports = class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) + this.isCaseInsensitive = fs.isCaseInsensitive() let {project, refreshOnWindowFocus} = options this.project = project @@ -96,7 +92,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }).then(() => { @@ -123,8 +119,9 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - let relativePath = this._gitUtilsRepo.relativize(_path) + let relativePath return this.repoPromise.then((repo) => { + relativePath = this.relativize(_path, repo.workdir()) return this._filterStatusesByPath(_path) }).then((statuses) => { let cachedStatus = this.pathStatusCache[relativePath] || 0 @@ -145,9 +142,10 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { - let relativePath = this._gitUtilsRepo.relativize(directoryPath) + let relativePath // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { + relativePath = this.relativize(directoryPath, repo.workdir()) return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { @@ -215,8 +213,52 @@ module.exports = class GitRepositoryAsync { return } + relativize (_path, workingDirectory) { + // Cargo-culted from git-utils. The original implementation also handles + // this.openedWorkingDirectory, which is set by git-utils when the + // repository is opened. Those branches of the if tree aren't included here + // yet, but if we determine we still need that here it should be simple to + // port. + // + // The original implementation also handled null workingDirectory as it + // pulled it from a sync function that could return null. We require it + // to be passed here. + if (!_path || !workingDirectory) { + return _path + } + + if (process.platform === 'win32') { + _path = _path.replace(/\\/g, '/') + } else { + if (_path[0] !== '/') { + return _path + } + } + + if (!/\/$/.test(workingDirectory)) { + workingDirectory = `${workingDirectory}/` + } + + if (this.isCaseInsensitive) { + let lowerCasePath = _path.toLowerCase() + + workingDirectory = workingDirectory.toLowerCase() + if (lowerCasePath.indexOf(workingDirectory) === 0) { + return _path.substring(workingDirectory.length) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + + return _path + } + getCachedPathStatus (_path) { - return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] + return this.repoPromise.then((repo) => { + return this.pathStatusCache[this.relativize(_path, repo.workdir())] + }) } isStatusNew (statusBit) { From 55a1d3b75f530ea3217690314e5195744546d5a3 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 16:06:18 +0100 Subject: [PATCH 082/254] Use async repo, add missing return --- spec/git-repository-async-spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 5b1050555..affbfc435 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -312,6 +312,8 @@ describe('GitRepositoryAsync-js', () => { // to complete before we continue. let repository = atom.project.getRepositories()[0] waitsForPromise(() => repository.refreshStatus()) + let repository = atom.project.getRepositories()[0].async + waitsForPromise(() => { return repository.refreshStatus }) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { From 861d7550b58d4ee06cb407748fc281508566397a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 16:09:57 +0100 Subject: [PATCH 083/254] i am bad at using GH desktop --- spec/git-repository-async-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index affbfc435..cb8157fa9 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -310,8 +310,6 @@ describe('GitRepositoryAsync-js', () => { // we're in a known state. *But* it's really hard to observe that from the // outside in a non-racy fashion. So let's refresh again and wait for it // to complete before we continue. - let repository = atom.project.getRepositories()[0] - waitsForPromise(() => repository.refreshStatus()) let repository = atom.project.getRepositories()[0].async waitsForPromise(() => { return repository.refreshStatus }) }) From 2246cca63119f59c5d460f6c269c1a7558b35de6 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 16:11:17 +0100 Subject: [PATCH 084/254] Fix typo --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index cb8157fa9..7ebcd8210 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -311,7 +311,7 @@ describe('GitRepositoryAsync-js', () => { // outside in a non-racy fashion. So let's refresh again and wait for it // to complete before we continue. let repository = atom.project.getRepositories()[0].async - waitsForPromise(() => { return repository.refreshStatus }) + waitsForPromise(() => { return repository.refreshStatus() }) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { From e7fe7f92e085fb1f77d7ba05fd32ad1fbd3c26f0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 11 Nov 2015 11:20:25 -0500 Subject: [PATCH 085/254] We don't need node-pre-gyp as a dependency. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3f346b691..62139af29 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "node-pre-gyp": "^0.6.15", "nodegit": "~0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", From 9e2a65a294bb580d30aa103d2782bc2628b265e3 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 18:33:56 +0100 Subject: [PATCH 086/254] Add cache config from @joefitzgerald's branch. If this doesn't speed us up enough we'll cache node_modules as well :godmode: --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index bf1bd330e..eabdd483d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,12 @@ install: script: script/cibuild +cache: + directories: + - $HOME/.atom/.apm + - $HOME/.atom/.node-gyp/.atom + - $HOME/.atom/.npm + notifications: email: on_success: never From a8c297e7f2a73c128c157020c7b180d0af608107 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 17 Nov 2015 15:32:35 -0800 Subject: [PATCH 087/254] Don't need these, we'll use the JS one. --- spec/git-repository-async-spec.coffee | 460 -------------------------- 1 file changed, 460 deletions(-) delete mode 100644 spec/git-repository-async-spec.coffee diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee deleted file mode 100644 index c6402d1fb..000000000 --- a/spec/git-repository-async-spec.coffee +++ /dev/null @@ -1,460 +0,0 @@ -temp = require 'temp' -GitRepositoryAsync = require '../src/git-repository-async' -Git = require 'nodegit' -fs = require 'fs-plus' -os = require 'os' -path = require 'path' -Task = require '../src/task' -Project = require '../src/project' - -# Clean up when the process exits -temp.track() - -copyRepository = -> - workingDirPath = temp.mkdirSync('atom-working-dir') - fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) - fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - fs.realpathSync(workingDirPath) - -openFixture = (fixture) -> - GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) - -describe "GitRepositoryAsync", -> - repo = null - - # beforeEach -> - # gitPath = path.join(temp.dir, '.git') - # fs.removeSync(gitPath) if fs.isDirectorySync(gitPath) - # - # afterEach -> - # repo.destroy() if repo?.repo? - - describe "@open(path)", -> - - # This just exercises the framework, but I'm trying to match the sync specs to start - it "repo is null when no repository is found", -> - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - - waitsForPromise {shouldReject: true}, -> - repo.repoPromise - - runs -> - expect(repo.repo).toBe null - - describe ".getPath()", -> - # XXX HEAD isn't a git directory.. what's this spec supposed to be about? - xit "returns the repository path for a .git directory path", -> - # Rejects as malformed - repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) - - onSuccess = jasmine.createSpy('onSuccess') - - waitsForPromise -> - repo.getPath().then(onSuccess) - - runs -> - expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) - - it "returns the repository path for a repository path", -> - repo = openFixture('master.git') - - onSuccess = jasmine.createSpy('onSuccess') - - waitsForPromise -> - repo.getPath().then(onSuccess) - - runs -> - expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) - - describe ".isPathIgnored(path)", -> - it "resolves true for an ignored path", -> - repo = openFixture('ignore.git') - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathIgnored('a.txt').then(onSuccess).catch (e) -> console.log e - - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "resolves false for a non-ignored path", -> - repo = openFixture('ignore.git') - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathIgnored('b.txt').then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - - describe ".isPathModified(path)", -> - [repo, filePath, newPath, emptyPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - emptyPath = path.join(workingDirPath, 'empty-path.txt') - - describe "when the path is unstaged", -> - it "resolves false if the path has not been modified", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - it "resolves true if the path is modified", -> - fs.writeFileSync(filePath, "change") - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "resolves false if the path is new", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(newPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - it "resolves false if the path is invalid", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(emptyPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - describe ".isPathNew(path)", -> - [filePath, newPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(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", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathNew(newPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "returns false if the path isn't new", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(newPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - - describe ".checkoutHead(path)", -> - [filePath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - - it "no longer reports a path as modified after checkout", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - fs.writeFileSync(filePath, 'ch ch changes') - - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - # Don't need to assert that this succeded because waitsForPromise should - # fail if it was rejected.. - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - onSuccess = jasmine.createSpy('onSuccess') - - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - it "restores the contents of the path to the original text", -> - fs.writeFileSync(filePath, 'ch ch changes') - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - it "fires a did-change-status event if the checkout completes successfully", -> - fs.writeFileSync(filePath, 'ch ch changes') - statusHandler = jasmine.createSpy('statusHandler') - - waitsForPromise -> - repo.getPathStatus(filePath) - runs -> - repo.onDidChangeStatus statusHandler - - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} - - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - expect(statusHandler.callCount).toBe 1 - - xdescribe ".checkoutHeadForEditor(editor)", -> - [filePath, editor] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - waitsForPromise -> - atom.workspace.open(filePath) - - runs -> - editor = atom.workspace.getActiveTextEditor() - - xit "displays a confirmation dialog by default", -> - spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - waitsForPromise -> - repo.checkoutHeadForEditor(editor) - runs -> - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - xit "does not display a dialog when confirmation is disabled", -> - spyOn(atom, 'confirm') - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - waitsForPromise -> - repo.checkoutHeadForEditor(editor) - runs -> - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - expect(atom.confirm).not.toHaveBeenCalled() - - xdescribe ".destroy()", -> - it "throws an exception when any method is called after it is called", -> - repo = new GitRepository(require.resolve('./fixtures/git/master.git/HEAD')) - repo.destroy() - expect(-> repo.getShortHead()).toThrow() - - describe ".getPathStatus(path)", -> - [filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - - it "trigger a status-changed event when the new status differs from the last cached one", -> - statusHandler = jasmine.createSpy("statusHandler") - repo.onDidChangeStatus statusHandler - fs.writeFileSync(filePath, '') - - waitsForPromise -> - repo.getPathStatus(filePath) - - runs -> - expect(statusHandler.callCount).toBe 1 - status = Git.Status.STATUS.WT_MODIFIED - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} - fs.writeFileSync(filePath, 'abc') - - waitsForPromise -> - status = repo.getPathStatus(filePath) - - runs -> - expect(statusHandler.callCount).toBe 1 - - describe ".getDirectoryStatus(path)", -> - [directoryPath, filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - directoryPath = path.join(workingDirectory, 'dir') - filePath = path.join(directoryPath, 'b.txt') - - it "gets the status based on the files inside the directory", -> - onSuccess = jasmine.createSpy('onSuccess') - onSuccess2 = jasmine.createSpy('onSuccess2') - - waitsForPromise -> - repo.getDirectoryStatus(directoryPath).then(onSuccess) - - runs -> - expect(onSuccess.callCount).toBe 1 - expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false - fs.writeFileSync(filePath, 'abc') - - waitsForPromise -> - repo.getDirectoryStatus(directoryPath).then(onSuccess2) - runs -> - expect(onSuccess2.callCount).toBe 1 - expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true - - - describe ".refreshStatus()", -> - [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - 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 # specs could be running under symbol path. - - it "returns status information for all new and modified files", -> - fs.writeFileSync(modifiedPath, 'making this path modified') - statusHandler = jasmine.createSpy('statusHandler') - onSuccess = jasmine.createSpy('onSuccess') - repo.onDidChangeStatuses statusHandler - waitsForPromise -> - repo.refreshStatus().then(onSuccess) - - runs -> - # Callers will use the promise returned by refreshStatus, not the - # cache directly - expect(onSuccess.mostRecentCall.args[0]).toEqual(repo.pathStatusCache) - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - - # This tests the async implementation's events directly, but ultimately I - # think we want users to just be able to subscribe to events on GitRepository - # and have them bubble up from async-land - - describe "buffer events", -> - [editor] = [] - - beforeEach -> - atom.project.setPaths([copyRepository()]) - - it "emits a status-changed event when a buffer is saved", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - editor.insertNewline() - statusHandler = jasmine.createSpy('statusHandler') - repo = atom.project.getRepositories()[0] - repo.async.onDidChangeStatus statusHandler - editor.save() - - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - it "emits a status-changed event when a buffer is reloaded", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().reload() - reloadHandler = jasmine.createSpy 'reloadHandler' - - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() - - waitsFor -> - reloadHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - - it "emits a status-changed event when a buffer's path changes", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().emitter.emit 'did-change-path' - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - pathHandler = jasmine.createSpy('pathHandler') - buffer = editor.getBuffer() - buffer.onDidChangePath pathHandler - buffer.emitter.emit 'did-change-path' - waitsFor -> - pathHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - - it "stops listening to the buffer when the repository is destroyed (regression)", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - atom.project.getRepositories()[0].destroy() - expect(-> editor.save()).not.toThrow() - - describe "when a project is deserialized", -> - [buffer, project2] = [] - - afterEach -> - project2?.destroy() - - it "subscribes to all the serialized buffers in the project", -> - atom.project.setPaths([copyRepository()]) - - waitsForPromise -> - atom.workspace.open('file.txt') - - runs -> - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - project2.deserialize(atom.project.serialize(), atom.deserializers) - buffer = project2.getBuffers()[0] - - waitsFor -> - buffer.loaded - - runs -> - originalContent = buffer.getText() - buffer.append('changes') - - statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].async.onDidChangeStatus statusHandler - buffer.save() - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} From d54e3f2d3c0b516be53b8151a6b1441ad12d6538 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 17 Nov 2015 15:37:58 -0800 Subject: [PATCH 088/254] Implement the Windows focus refreshing. --- src/git-repository-async.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fa5518d85..d0c7c2323 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,7 +3,7 @@ const fs = require('fs-plus') const Git = require('nodegit') const path = require('path') -const {Emitter, CompositeDisposable} = require('event-kit') +const {Emitter, CompositeDisposable, Disposable} = require('event-kit') const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -37,7 +37,11 @@ module.exports = class GitRepositoryAsync { refreshOnWindowFocus = true } if (refreshOnWindowFocus) { - // TODO + const onWindowFocus = () => { + this.refreshStatus() + } + window.addEventListener('focus', onWindowFocus) + this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) } if (this.project) { From 8c47335db6f913a8493a1bdf852b86d2702cdf6b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 17 Nov 2015 15:38:10 -0800 Subject: [PATCH 089/254] ES6 harder. --- src/git-repository-async.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d0c7c2323..1969a9c6b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -343,17 +343,17 @@ module.exports = class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.repoPromise.then(function (repo) { - return repo.openIndex() - }).then(function (index) { - let entry = index.getByPath(_path) - let submoduleMode = 57344 // TODO compose this from libgit2 constants + return this.repoPromise + .then(repo => repo.openIndex()) + .then(index => { + let entry = index.getByPath(_path) + let submoduleMode = 57344 // TODO compose this from libgit2 constants - if (entry.mode === submoduleMode) { - return true - } else { - return false - } - }) + if (entry.mode === submoduleMode) { + return true + } else { + return false + } + }) } } From e1aa23b92d1f46bd525643abcdc5595f4f31ad10 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 30 Nov 2015 11:47:06 -0500 Subject: [PATCH 090/254] Const and comparison fixes. h/t @YuriSolovyov --- src/git-repository-async.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1969a9c6b..4051f5df6 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -346,14 +346,9 @@ module.exports = class GitRepositoryAsync { return this.repoPromise .then(repo => repo.openIndex()) .then(index => { - let entry = index.getByPath(_path) - let submoduleMode = 57344 // TODO compose this from libgit2 constants - - if (entry.mode === submoduleMode) { - return true - } else { - return false - } + const entry = index.getByPath(_path) + const submoduleMode = 57344 // TODO compose this from libgit2 constants + return entry.mode === submoduleMode }) } } From ee3b65506725e9940efe62f4c5e307d4c63eb4dc Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 12:19:51 -0500 Subject: [PATCH 091/254] git-repository will refresh us on window focus. --- src/git-repository-async.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4051f5df6..eea06011c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,7 +3,7 @@ const fs = require('fs-plus') const Git = require('nodegit') const path = require('path') -const {Emitter, CompositeDisposable, Disposable} = require('event-kit') +const {Emitter, CompositeDisposable} = require('event-kit') const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -31,18 +31,8 @@ module.exports = class GitRepositoryAsync { this.repoPromise = Git.Repository.open(path) this.isCaseInsensitive = fs.isCaseInsensitive() - let {project, refreshOnWindowFocus} = options + let {project} = options this.project = project - if (refreshOnWindowFocus === undefined) { - refreshOnWindowFocus = true - } - if (refreshOnWindowFocus) { - const onWindowFocus = () => { - this.refreshStatus() - } - window.addEventListener('focus', onWindowFocus) - this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) - } if (this.project) { this.subscriptions.add(this.project.onDidAddBuffer((buffer) => { From c76d0f04382865f922096dae0272ffa2da8b8c18 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 12:22:25 -0500 Subject: [PATCH 092/254] Update the current branch as well. --- src/git-repository-async.js | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index eea06011c..3a2982efa 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -120,7 +120,7 @@ module.exports = class GitRepositoryAsync { }).then((statuses) => { let cachedStatus = this.pathStatusCache[relativePath] || 0 let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus) { + if (status !== cachedStatus && this.emitter != null) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status @@ -156,26 +156,39 @@ module.exports = class GitRepositoryAsync { }) } + // Get the current branch and update this.branch. + // + // Returns :: Promise + // The branch name. + _refreshBranch () { + return this.repoPromise + .then(repo => repo.getCurrentBranch()) + .then(ref => ref.name()) + .then(branchRef => this.branch = branchRef) + } + // Refreshes the git status. Note: the sync GitRepository class does this with // a separate process, let's see if we can avoid that. refreshStatus () { // TODO add upstream, branch, and submodule tracking - return this.repoPromise.then((repo) => { - return repo.getStatus() - }).then((statuses) => { - // update the status cache - return Promise.all(statuses.map((status) => { - return [status.path(), status.statusBit()] - })).then((statusesByPath) => { - return _.object(statusesByPath) + const status = this.repoPromise + .then(repo => repo.getStatus()) + .then(statuses => { + // update the status cache + return Promise.all(statuses.map(status => [status.path(), status.statusBit()])) + .then(statusesByPath => _.object(statusesByPath)) }) - }).then((newPathStatusCache) => { - if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { - this.emitter.emit('did-change-statuses') - } - this.pathStatusCache = newPathStatusCache - return newPathStatusCache - }) + .then(newPathStatusCache => { + if (!_.isEqual(this.pathStatusCache, newPathStatusCache) && this.emitter != null) { + this.emitter.emit('did-change-statuses') + } + this.pathStatusCache = newPathStatusCache + return newPathStatusCache + }) + + const branch = this._refreshBranch() + + return Promise.all([status, branch]) } // Section: Private From 64d471600337222ac0a0ff07891140807c1b2471 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 12:47:11 -0500 Subject: [PATCH 093/254] Use modern imports. --- spec/git-repository-async-spec.js | 74 ++++++++++++------------------- src/git-repository-async.js | 10 ++--- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 7ebcd8210..2e6c1188e 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -1,12 +1,14 @@ 'use babel' -const fs = require('fs-plus') -const path = require('path') -const temp = require('temp') -const Git = require('nodegit') +import fs from 'fs-plus' +import path from 'path' +import temp from 'temp' +import Git from 'nodegit' -const GitRepositoryAsync = require('../src/git-repository-async') -const Project = require('../src/project') +import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' + +import GitRepositoryAsync from '../src/git-repository-async' +import Project from '../src/project' temp.track() @@ -21,24 +23,6 @@ function copyRepository() { return fs.realpathSync(workingDirPath) } -function asyncIt(name, fn) { - it(name, () => { - waitsForPromise(fn) - }) -} - -function fasyncIt(name, fn) { - fit(name, () => { - waitsForPromise(fn) - }) -} - -function xasyncIt(name, fn) { - xit(name, () => { - waitsForPromise(fn) - }) -} - describe('GitRepositoryAsync-js', () => { let repo @@ -47,7 +31,7 @@ describe('GitRepositoryAsync-js', () => { }) describe('@open(path)', () => { - asyncIt('repo is null when no repository is found', async () => { + it('repo is null when no repository is found', async () => { repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) let threw = false @@ -65,7 +49,7 @@ describe('GitRepositoryAsync-js', () => { describe('.getPath()', () => { xit('returns the repository path for a .git directory path') - asyncIt('returns the repository path for a repository path', async () => { + it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') let repoPath = await repo.getPath() expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) @@ -73,13 +57,13 @@ describe('GitRepositoryAsync-js', () => { }) describe('.isPathIgnored(path)', () => { - asyncIt('resolves true for an ignored path', async () => { + it('resolves true for an ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('a.txt') expect(ignored).toBeTruthy() }) - asyncIt('resolves false for a non-ignored path', async () => { + it('resolves false for a non-ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('b.txt') expect(ignored).toBeFalsy() @@ -99,23 +83,23 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - asyncIt('resolves false if the path has not been modified', async () => { + it('resolves false if the path has not been modified', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) - asyncIt('resolves true if the path is modified', async () => { + it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, "change") let modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) - asyncIt('resolves false if the path is new', async () => { + it('resolves false if the path is new', async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) - asyncIt('resolves false if the path is invalid', async () => { + it('resolves false if the path is invalid', async () => { let modified = await repo.isPathModified(emptyPath) expect(modified).toBeFalsy() }) @@ -134,12 +118,12 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - asyncIt('returns true if the path is new', async () => { + it('returns true if the path is new', async () => { let isNew = await repo.isPathNew(newPath) expect(isNew).toBeTruthy() }) - asyncIt("returns false if the path isn't new", async () => { + it("returns false if the path isn't new", async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) @@ -155,7 +139,7 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirPath, 'a.txt') }) - asyncIt('no longer reports a path as modified after checkout', async () => { + it('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() @@ -170,13 +154,13 @@ describe('GitRepositoryAsync-js', () => { expect(modified).toBeFalsy() }) - asyncIt('restores the contents of the path to the original text', async () => { + it('restores the contents of the path to the original text', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.checkoutHead(filePath) expect(fs.readFileSync(filePath, 'utf8')).toBe('') }) - asyncIt('fires a did-change-status event if the checkout completes successfully', async () => { + it('fires a did-change-status event if the checkout completes successfully', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.getPathStatus(filePath) @@ -236,7 +220,7 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirectory, 'file.txt') }) - asyncIt('trigger a status-changed event when the new status differs from the last cached one', async () => { + it('trigger a status-changed event when the new status differs from the last cached one', async () => { let statusHandler = jasmine.createSpy("statusHandler") repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') @@ -263,7 +247,7 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(directoryPath, 'b.txt') }) - asyncIt('gets the status based on the files inside the directory', async () => { + it('gets the status based on the files inside the directory', async () => { await repo.checkoutHead(filePath) let result = await repo.getDirectoryStatus(directoryPath) @@ -290,7 +274,7 @@ describe('GitRepositoryAsync-js', () => { newPath = fs.absolute(newPath) // specs could be running under symbol path. }) - asyncIt('returns status information for all new and modified files', async () => { + it('returns status information for all new and modified files', async () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() @@ -314,7 +298,7 @@ describe('GitRepositoryAsync-js', () => { waitsForPromise(() => { return repository.refreshStatus() }) }) - asyncIt('emits a status-changed event when a buffer is saved', async () => { + it('emits a status-changed event when a buffer is saved', async () => { let editor = await atom.workspace.open('other.txt') editor.insertNewline() @@ -328,7 +312,7 @@ describe('GitRepositoryAsync-js', () => { runs(() => expect(called).toEqual({path: editor.getPath(), pathStatus: 256})) }) - asyncIt('emits a status-changed event when a buffer is reloaded', async () => { + it('emits a status-changed event when a buffer is reloaded', async () => { let statusHandler = jasmine.createSpy('statusHandler') let reloadHandler = jasmine.createSpy('reloadHandler') @@ -354,7 +338,7 @@ describe('GitRepositoryAsync-js', () => { }) }) - asyncIt("emits a status-changed event when a buffer's path changes", async () => { + it("emits a status-changed event when a buffer's path changes", async () => { let editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') @@ -378,7 +362,7 @@ describe('GitRepositoryAsync-js', () => { }) }) - asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + it('stops listening to the buffer when the repository is destroyed (regression)', async () => { let editor = await atom.workspace.open('other.txt') atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() @@ -401,7 +385,7 @@ describe('GitRepositoryAsync-js', () => { if (project2) project2.destroy() }) - asyncIt('subscribes to all the serialized buffers in the project', async () => { + it('subscribes to all the serialized buffers in the project', async () => { await atom.workspace.open('file.txt') project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 3a2982efa..1022f8cc2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,9 +1,9 @@ 'use babel' -const fs = require('fs-plus') -const Git = require('nodegit') -const path = require('path') -const {Emitter, CompositeDisposable} = require('event-kit') +import fs from 'fs-plus' +import Git from 'nodegit' +import path from 'path' +import {Emitter, CompositeDisposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -11,7 +11,7 @@ const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDE const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE // Just using this for _.isEqual and _.object, we should impl our own here -const _ = require('underscore-plus') +import _ from 'underscore-plus' module.exports = class GitRepositoryAsync { static open (path, options = {}) { From 24b8fcfc88791feaededf994dafd9fd4fd47b9bb Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:36:23 -0500 Subject: [PATCH 094/254] Gimme the linters. --- build/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/package.json b/build/package.json index 40e5b309a..9996dec81 100644 --- a/build/package.json +++ b/build/package.json @@ -9,6 +9,7 @@ "asar": "^0.8.0", "async": "~0.2.9", "aws-sdk": "^2.2.18", + "babel-eslint": "^5.0.0-beta4", "donna": "^1.0.13", "formidable": "~1.0.14", "fs-plus": "2.x", @@ -35,6 +36,7 @@ "request": "~2.27.0", "rimraf": "~2.2.2", "runas": "^3.1", + "standard": "^5.4.1", "tello": "1.0.5", "temp": "~0.8.1", "underscore-plus": "1.x", From cf30f39047fb9d33f6ac7163f5c22fb6674fd7b1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:36:28 -0500 Subject: [PATCH 095/254] These are fine. --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 5f16bf814..c87f4723d 100644 --- a/package.json +++ b/package.json @@ -163,8 +163,12 @@ "afterEach", "beforeEach", "describe", + "fdescribe", + "xdescribe", "expect", "it", + "fit", + "xit", "jasmine", "runs", "spyOn", From f290596542d5f10b6bc741b0f5a73d68b757a60c Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:36:48 -0500 Subject: [PATCH 096/254] Remove some lint. --- spec/git-repository-async-spec.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2e6c1188e..26d767bd4 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -5,18 +5,18 @@ import path from 'path' import temp from 'temp' import Git from 'nodegit' -import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' +import {it, beforeEach, afterEach} from './async-spec-helpers' import GitRepositoryAsync from '../src/git-repository-async' import Project from '../src/project' temp.track() -function openFixture(fixture) { +function openFixture (fixture) { return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } -function copyRepository() { +function copyRepository () { let workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) @@ -37,7 +37,7 @@ describe('GitRepositoryAsync-js', () => { let threw = false try { await repo.repoPromise - } catch(e) { + } catch (e) { threw = true } @@ -89,7 +89,7 @@ describe('GitRepositoryAsync-js', () => { }) it('resolves true if the path is modified', async () => { - fs.writeFileSync(filePath, "change") + fs.writeFileSync(filePath, 'change') let modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) @@ -107,12 +107,11 @@ describe('GitRepositoryAsync-js', () => { }) describe('.isPathNew(path)', () => { - let filePath, newPath + let newPath beforeEach(() => { let workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") }) @@ -192,7 +191,7 @@ describe('GitRepositoryAsync-js', () => { }) xit('displays a confirmation dialog by default', () => { - spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) + spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) // eslint-disable-line atom.config.set('editor.confirmCheckoutHeadRevision', true) waitsForPromise(() => repo.checkoutHeadForEditor(editor)) @@ -221,7 +220,7 @@ describe('GitRepositoryAsync-js', () => { }) it('trigger a status-changed event when the new status differs from the last cached one', async () => { - let statusHandler = jasmine.createSpy("statusHandler") + let statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') @@ -261,7 +260,7 @@ describe('GitRepositoryAsync-js', () => { }) describe('.refreshStatus()', () => { - let newPath, modifiedPath, cleanPath, originalModifiedPathText + let newPath, modifiedPath, cleanPath beforeEach(() => { let workingDirectory = copyRepository() @@ -435,7 +434,5 @@ describe('GitRepositoryAsync-js', () => { let relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) - }) - }) From 4e64af91556e29b378936c7d8368148a3224c9c3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:53:57 -0500 Subject: [PATCH 097/254] ES6 some more. --- src/git-repository-async.js | 140 +++++++++++++++++------------------- 1 file changed, 64 insertions(+), 76 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1022f8cc2..fa568edc9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -31,15 +31,15 @@ module.exports = class GitRepositoryAsync { this.repoPromise = Git.Repository.open(path) this.isCaseInsensitive = fs.isCaseInsensitive() - let {project} = options + const {project} = options this.project = project if (this.project) { - this.subscriptions.add(this.project.onDidAddBuffer((buffer) => { + this.subscriptions.add(this.project.onDidAddBuffer(buffer => { this.subscribeToBuffer(buffer) })) - this.project.getBuffers().forEach((buffer) => { this.subscribeToBuffer(buffer) }) + this.project.getBuffers().forEach(buffer => { this.subscribeToBuffer(buffer) }) } } @@ -56,47 +56,39 @@ module.exports = class GitRepositoryAsync { } getPath () { - return this.repoPromise.then((repo) => { - return repo.path().replace(/\/$/, '') - }) + return this.repoPromise.then(repo => repo.path().replace(/\/$/, '')) } isPathIgnored (_path) { - return this.repoPromise.then((repo) => { - return Git.Ignore.pathIsIgnored(repo, _path) - }) + return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) } isPathModified (_path) { - return this._filterStatusesByPath(_path).then(function (statuses) { - return statuses.filter((status) => { - return status.isModified() - }).length > 0 + return this._filterStatusesByPath(_path).then(statuses => { + return statuses.filter(status => status.isModified()).length > 0 }) } isPathNew (_path) { - return this._filterStatusesByPath(_path).then(function (statuses) { - return statuses.filter((status) => { - return status.isNew() - }).length > 0 + return this._filterStatusesByPath(_path).then(statuses => { + return statuses.filter(status => status.isNew()).length > 0 }) } checkoutHead (_path) { - return this.repoPromise.then((repo) => { - let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, repo.workdir())] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) - }).then(() => { - return this.getPathStatus(_path) - }) + return this.repoPromise + .then(repo => { + let checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + return Git.Checkout.head(repo, checkoutOptions) + }) + .then(() => this.getPathStatus(_path)) } checkoutHeadForEditor (editor) { - return new Promise(function (resolve, reject) { - let filePath = editor.getPath() + return new Promise((resolve, reject) => { + const filePath = editor.getPath() if (filePath) { if (editor.buffer.isModified()) { editor.buffer.reload() @@ -105,27 +97,27 @@ module.exports = class GitRepositoryAsync { } else { reject() } - }).then((filePath) => { - return this.checkoutHead(filePath) - }) + }).then(filePath => this.checkoutHead(filePath)) } // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { let relativePath - return this.repoPromise.then((repo) => { - relativePath = this.relativize(_path, repo.workdir()) - return this._filterStatusesByPath(_path) - }).then((statuses) => { - let cachedStatus = this.pathStatusCache[relativePath] || 0 - let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus && this.emitter != null) { - this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) - } - this.pathStatusCache[relativePath] = status - return status - }) + return this.repoPromise + .then(repo => { + relativePath = this.relativize(_path, repo.workdir()) + return this._filterStatusesByPath(_path) + }) + .then(statuses => { + const cachedStatus = this.pathStatusCache[relativePath] || 0 + const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + if (status !== cachedStatus && this.emitter != null) { + this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) + } + this.pathStatusCache[relativePath] = status + return status + }) } // Get the status of a directory in the repository's working directory. @@ -138,22 +130,22 @@ module.exports = class GitRepositoryAsync { getDirectoryStatus (directoryPath) { let relativePath // XXX _filterSBD already gets repoPromise - return this.repoPromise.then((repo) => { - relativePath = this.relativize(directoryPath, repo.workdir()) - return this._filterStatusesByDirectory(relativePath) - }).then((statuses) => { - return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { - let directoryStatus = 0 - let filteredBits = bits.filter(function (b) { return b > 0 }) - if (filteredBits.length > 0) { - filteredBits.forEach(function (bit) { - directoryStatus |= bit - }) - } - - return directoryStatus + return this.repoPromise + .then(repo => { + relativePath = this.relativize(directoryPath, repo.workdir()) + return this._filterStatusesByDirectory(relativePath) + }) + .then(statuses => { + return Promise.all(statuses.map(s => s.statusBit())).then(bits => { + let directoryStatus = 0 + const filteredBits = bits.filter(b => b > 0) + if (filteredBits.length > 0) { + filteredBits.forEach(bit => directoryStatus |= bit) + } + + return directoryStatus + }) }) - }) } // Get the current branch and update this.branch. @@ -195,7 +187,7 @@ module.exports = class GitRepositoryAsync { // ================ subscribeToBuffer (buffer) { - let bufferSubscriptions = new CompositeDisposable() + const bufferSubscriptions = new CompositeDisposable() let getBufferPathStatus = () => { let _path = buffer.getPath() @@ -247,7 +239,7 @@ module.exports = class GitRepositoryAsync { } if (this.isCaseInsensitive) { - let lowerCasePath = _path.toLowerCase() + const lowerCasePath = _path.toLowerCase() workingDirectory = workingDirectory.toLowerCase() if (lowerCasePath.indexOf(workingDirectory) === 0) { @@ -263,7 +255,7 @@ module.exports = class GitRepositoryAsync { } getCachedPathStatus (_path) { - return this.repoPromise.then((repo) => { + return this.repoPromise.then(repo => { return this.pathStatusCache[this.relativize(_path, repo.workdir())] }) } @@ -291,24 +283,22 @@ module.exports = class GitRepositoryAsync { _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this let basePath = null - return this.repoPromise.then((repo) => { - basePath = repo.workdir() - return repo.getStatus() - }).then((statuses) => { - return statuses.filter(function (status) { - return _path === path.join(basePath, status.path()) + return this.repoPromise + .then(repo => { + basePath = repo.workdir() + return repo.getStatus() + }) + .then(statuses => { + return statuses.filter(status => _path === path.join(basePath, status.path())) }) - }) } _filterStatusesByDirectory (directoryPath) { - return this.repoPromise.then(function (repo) { - return repo.getStatus() - }).then(function (statuses) { - return statuses.filter((status) => { - return status.path().indexOf(directoryPath) === 0 + return this.repoPromise + .then(repo => repo.getStatus()) + .then(statuses => { + return statuses.filter(status => status.path().indexOf(directoryPath) === 0) }) - }) } // Event subscription // ================== @@ -334,9 +324,7 @@ module.exports = class GitRepositoryAsync { isProjectAtRoot () { if (this.projectAtRoot === undefined) { this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then((repo) => { - return this.project.relativize(repo.workdir) - }) + return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) }) } From f6a00458cb3d35285bde88230c8e755121990124 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:59:29 -0500 Subject: [PATCH 098/254] License overrides. --- build/tasks/license-overrides.coffee | 203 +++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index e716f76a2..173e41144 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -123,3 +123,206 @@ module.exports = (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + 'tweetnacl@0.13.2': + license: 'CC0-1.0' + source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.' + 'json-schema@0.2.2': + license: 'AFLv2.1' + source: 'README links to https://github.com/dojo/dojo/blob/8b6a5e4c42f9cf777dd39eaae8b188e0ebb59a4c/LICENSE' + sourceText: """ + Dojo is available under *either* the terms of the modified BSD license *or* the + Academic Free License version 2.1. As a recipient of Dojo, you may choose which + license to receive this code under (except as noted in per-module LICENSE + files). Some modules may not be the copyright of the Dojo Foundation. These + modules contain explicit declarations of copyright in both the LICENSE files in + the directories in which they reside and in the code itself. No external + contributions are allowed under licenses which are fundamentally incompatible + with the AFL or BSD licenses that Dojo is distributed under. + + The text of the AFL and BSD licenses is reproduced below. + + ------------------------------------------------------------------------------- + The "New" BSD License: + ********************** + + Copyright (c) 2005-2015, The Dojo Foundation + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Dojo Foundation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ------------------------------------------------------------------------------- + The Academic Free License, v. 2.1: + ********************************** + + This Academic Free License (the "License") applies to any original work of + authorship (the "Original Work") whose owner (the "Licensor") has placed the + following notice immediately following the copyright notice for the Original + Work: + + Licensed under the Academic Free License version 2.1 + + 1) Grant of Copyright License. Licensor hereby grants You a world-wide, + royalty-free, non-exclusive, perpetual, sublicenseable license to do the + following: + + a) to reproduce the Original Work in copies; + + b) to prepare derivative works ("Derivative Works") based upon the Original + Work; + + c) to distribute copies of the Original Work and Derivative Works to the + public; + + d) to perform the Original Work publicly; and + + e) to display the Original Work publicly. + + 2) Grant of Patent License. Licensor hereby grants You a world-wide, + royalty-free, non-exclusive, perpetual, sublicenseable license, under patent + claims owned or controlled by the Licensor that are embodied in the Original + Work as furnished by the Licensor, to make, use, sell and offer for sale the + Original Work and Derivative Works. + + 3) Grant of Source Code License. The term "Source Code" means the preferred + form of the Original Work for making modifications to it and all available + documentation describing how to modify the Original Work. Licensor hereby + agrees to provide a machine-readable copy of the Source Code of the Original + Work along with each copy of the Original Work that Licensor distributes. + Licensor reserves the right to satisfy this obligation by placing a + machine-readable copy of the Source Code in an information repository + reasonably calculated to permit inexpensive and convenient access by You for as + long as Licensor continues to distribute the Original Work, and by publishing + the address of that information repository in a notice immediately following + the copyright notice that applies to the Original Work. + + 4) Exclusions From License Grant. Neither the names of Licensor, nor the names + of any contributors to the Original Work, nor any of their trademarks or + service marks, may be used to endorse or promote products derived from this + Original Work without express prior written permission of the Licensor. Nothing + in this License shall be deemed to grant any rights to trademarks, copyrights, + patents, trade secrets or any other intellectual property of Licensor except as + expressly stated herein. No patent license is granted to make, use, sell or + offer to sell embodiments of any patent claims other than the licensed claims + defined in Section 2. No right is granted to the trademarks of Licensor even if + such marks are included in the Original Work. Nothing in this License shall be + interpreted to prohibit Licensor from licensing under different terms from this + License any Original Work that Licensor otherwise would have a right to + license. + + 5) This section intentionally omitted. + + 6) Attribution Rights. You must retain, in the Source Code of any Derivative + Works that You create, all copyright, patent or trademark notices from the + Source Code of the Original Work, as well as any notices of licensing and any + descriptive text identified therein as an "Attribution Notice." You must cause + the Source Code for any Derivative Works that You create to carry a prominent + Attribution Notice reasonably calculated to inform recipients that You have + modified the Original Work. + + 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that + the copyright in and to the Original Work and the patent rights granted herein + by Licensor are owned by the Licensor or are sublicensed to You under the terms + of this License with the permission of the contributor(s) of those copyrights + and patent rights. Except as expressly stated in the immediately proceeding + sentence, the Original Work is provided under this License on an "AS IS" BASIS + and WITHOUT WARRANTY, either express or implied, including, without limitation, + the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. + This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No + license to Original Work is granted hereunder except under this disclaimer. + + 8) Limitation of Liability. Under no circumstances and under no legal theory, + whether in tort (including negligence), contract, or otherwise, shall the + Licensor be liable to any person for any direct, indirect, special, incidental, + or consequential damages of any character arising as a result of this License + or the use of the Original Work including, without limitation, damages for loss + of goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses. This limitation of liability shall not + apply to liability for death or personal injury resulting from Licensor's + negligence to the extent applicable law prohibits such limitation. Some + jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + + 9) Acceptance and Termination. If You distribute copies of the Original Work or + a Derivative Work, You must make a reasonable effort under the circumstances to + obtain the express assent of recipients to the terms of this License. Nothing + else but this License (or another written agreement between Licensor and You) + grants You permission to create Derivative Works based upon the Original Work + or to exercise any of the rights granted in Section 1 herein, and any attempt + to do so except under the terms of this License (or another written agreement + between Licensor and You) is expressly prohibited by U.S. copyright law, the + equivalent laws of other countries, and by international treaty. Therefore, by + exercising any of the rights granted to You in Section 1 herein, You indicate + Your acceptance of this License and all of its terms and conditions. + + 10) Termination for Patent Action. This License shall terminate automatically + and You may no longer exercise any of the rights granted to You by this License + as of the date You commence an action, including a cross-claim or counterclaim, + against Licensor or any licensee alleging that the Original Work infringes a + patent. This termination provision shall not apply for an action alleging + patent infringement by combinations of the Original Work with other software or + hardware. + + 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this + License may be brought only in the courts of a jurisdiction wherein the + Licensor resides or in which Licensor conducts its primary business, and under + the laws of that jurisdiction excluding its conflict-of-law provisions. The + application of the United Nations Convention on Contracts for the International + Sale of Goods is expressly excluded. Any use of the Original Work outside the + scope of this License or after its termination shall be subject to the + requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et + seq., the equivalent laws of other countries, and international treaty. This + section shall survive the termination of this License. + + 12) Attorneys Fees. In any action to enforce the terms of this License or + seeking damages relating thereto, the prevailing party shall be entitled to + recover its costs and expenses, including, without limitation, reasonable + attorneys' fees and costs incurred in connection with such action, including + any appeal of such action. This section shall survive the termination of this + License. + + 13) Miscellaneous. This License represents the complete agreement concerning + the subject matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent necessary to + make it enforceable. + + 14) Definition of "You" in This License. "You" throughout this License, whether + in upper or lower case, means an individual or a legal entity exercising rights + under, and complying with all of the terms of, this License. For legal + entities, "You" includes any entity that controls, is controlled by, or is + under common control with you. For purposes of this definition, "control" means + (i) the power, direct or indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (ii) ownership of fifty percent + (50%) or more of the outstanding shares, or (iii) beneficial ownership of such + entity. + + 15) Right to Use. You may use the Original Work in all ways not otherwise + restricted or conditioned by this License or by law, and Licensor promises not + to interfere with or be responsible for such uses by You. + + This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. + Permission is hereby granted to copy and distribute this license without + modification. This license may not be modified without the express written + permission of its copyright owner. + """ From 5019cc31ab407022412f579b2382e26d7f2ea48d Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 14:16:50 -0500 Subject: [PATCH 099/254] These can be const too. --- src/git-repository-async.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fa568edc9..b6ac9c08a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -78,7 +78,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise .then(repo => { - let checkoutOptions = new Git.CheckoutOptions() + const checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this.relativize(_path, repo.workdir())] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) @@ -189,8 +189,8 @@ module.exports = class GitRepositoryAsync { subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() - let getBufferPathStatus = () => { - let _path = buffer.getPath() + const getBufferPathStatus = () => { + const _path = buffer.getPath() if (_path) { // We don't need to do anything with this promise, we just want the // emitted event side effect From 4559936af1d5000eb9bb0c4a41525d3060f285e5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 16:45:02 -0500 Subject: [PATCH 100/254] More const'ing. --- spec/git-repository-async-spec.js | 99 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 26d767bd4..e6bdd801e 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -51,21 +51,25 @@ describe('GitRepositoryAsync-js', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') - let repoPath = await repo.getPath() + const repoPath = await repo.getPath() expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { - it('resolves true for an ignored path', async () => { + let repo + + beforeEach(() => { repo = openFixture('ignore.git') - let ignored = await repo.isPathIgnored('a.txt') + }) + + it('resolves true for an ignored path', async () => { + const ignored = await repo.isPathIgnored('a.txt') expect(ignored).toBeTruthy() }) it('resolves false for a non-ignored path', async () => { - repo = openFixture('ignore.git') - let ignored = await repo.isPathIgnored('b.txt') + const ignored = await repo.isPathIgnored('b.txt') expect(ignored).toBeFalsy() }) }) @@ -74,7 +78,7 @@ describe('GitRepositoryAsync-js', () => { let filePath, newPath, emptyPath beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') @@ -84,23 +88,23 @@ describe('GitRepositoryAsync-js', () => { describe('when the path is unstaged', () => { it('resolves false if the path has not been modified', async () => { - let modified = await repo.isPathModified(filePath) + const modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, 'change') - let modified = await repo.isPathModified(filePath) + const modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) it('resolves false if the path is new', async () => { - let modified = await repo.isPathModified(newPath) + const modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) it('resolves false if the path is invalid', async () => { - let modified = await repo.isPathModified(emptyPath) + const modified = await repo.isPathModified(emptyPath) expect(modified).toBeFalsy() }) }) @@ -110,7 +114,7 @@ describe('GitRepositoryAsync-js', () => { let newPath beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") @@ -118,12 +122,12 @@ describe('GitRepositoryAsync-js', () => { describe('when the path is unstaged', () => { it('returns true if the path is new', async () => { - let isNew = await repo.isPathNew(newPath) + const isNew = await repo.isPathNew(newPath) expect(isNew).toBeTruthy() }) it("returns false if the path isn't new", async () => { - let modified = await repo.isPathModified(newPath) + const modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) }) @@ -133,7 +137,7 @@ describe('GitRepositoryAsync-js', () => { let filePath beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') }) @@ -164,7 +168,7 @@ describe('GitRepositoryAsync-js', () => { await repo.getPathStatus(filePath) - let statusHandler = jasmine.createSpy('statusHandler') + const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) await repo.checkoutHead(filePath) @@ -181,7 +185,7 @@ describe('GitRepositoryAsync-js', () => { let filePath, editor beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') @@ -214,20 +218,20 @@ describe('GitRepositoryAsync-js', () => { let filePath beforeEach(() => { - let workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) filePath = path.join(workingDirectory, 'file.txt') }) it('trigger a status-changed event when the new status differs from the last cached one', async () => { - let statusHandler = jasmine.createSpy('statusHandler') + const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') await repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe(1) - let status = Git.Status.STATUS.WT_MODIFIED + const status = Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) fs.writeFileSync(filePath, 'abc') @@ -240,7 +244,7 @@ describe('GitRepositoryAsync-js', () => { let directoryPath, filePath beforeEach(() => { - let workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) directoryPath = path.join(workingDirectory, 'dir') filePath = path.join(directoryPath, 'b.txt') @@ -263,7 +267,7 @@ describe('GitRepositoryAsync-js', () => { let newPath, modifiedPath, cleanPath beforeEach(() => { - let workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) modifiedPath = path.join(workingDirectory, 'file.txt') newPath = path.join(workingDirectory, 'untracked.txt') @@ -284,6 +288,8 @@ describe('GitRepositoryAsync-js', () => { }) describe('buffer events', () => { + let repository + beforeEach(() => { const workingDirectory = copyRepository() atom.project.setPaths([workingDirectory]) @@ -293,16 +299,15 @@ describe('GitRepositoryAsync-js', () => { // we're in a known state. *But* it's really hard to observe that from the // outside in a non-racy fashion. So let's refresh again and wait for it // to complete before we continue. - let repository = atom.project.getRepositories()[0].async - waitsForPromise(() => { return repository.refreshStatus() }) + repository = atom.project.getRepositories()[0].async + waitsForPromise(() => repository.refreshStatus()) }) it('emits a status-changed event when a buffer is saved', async () => { - let editor = await atom.workspace.open('other.txt') + const editor = await atom.workspace.open('other.txt') editor.insertNewline() - let repository = atom.project.getRepositories()[0].async let called repository.onDidChangeStatus(c => called = c) editor.save() @@ -312,14 +317,11 @@ describe('GitRepositoryAsync-js', () => { }) it('emits a status-changed event when a buffer is reloaded', async () => { - let statusHandler = jasmine.createSpy('statusHandler') - let reloadHandler = jasmine.createSpy('reloadHandler') - - let editor = await atom.workspace.open('other.txt') + const editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') - let repository = atom.project.getRepositories()[0].async + const statusHandler = jasmine.createSpy('statusHandler') repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() @@ -328,7 +330,8 @@ describe('GitRepositoryAsync-js', () => { expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - let buffer = editor.getBuffer() + const buffer = editor.getBuffer() + const reloadHandler = jasmine.createSpy('reloadHandler') buffer.onDidReload(reloadHandler) buffer.reload() @@ -338,32 +341,32 @@ describe('GitRepositoryAsync-js', () => { }) it("emits a status-changed event when a buffer's path changes", async () => { - let editor = await atom.workspace.open('other.txt') + const editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') - let statusHandler = jasmine.createSpy('statusHandler') - let repository = atom.project.getRepositories()[0].async + const statusHandler = jasmine.createSpy('statusHandler') repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') + waitsFor(() => statusHandler.callCount > 0) runs(() => { expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - let pathHandler = jasmine.createSpy('pathHandler') - let buffer = editor.getBuffer() + const pathHandler = jasmine.createSpy('pathHandler') + const buffer = editor.getBuffer() buffer.onDidChangePath(pathHandler) buffer.emitter.emit('did-change-path') waitsFor(() => pathHandler.callCount > 0) - runs(() => expect(statusHandler.callCount).toBe(1)) + runs(() => expect(pathHandler.callCount).toBe(1)) }) }) it('stops listening to the buffer when the repository is destroyed (regression)', async () => { - let editor = await atom.workspace.open('other.txt') - atom.project.getRepositories()[0].destroy() + const editor = await atom.workspace.open('other.txt') + repository.destroy() expect(() => editor.save()).not.toThrow() }) }) @@ -376,7 +379,7 @@ describe('GitRepositoryAsync-js', () => { // See the comment in the 'buffer events' beforeEach for why we need to do // this. - let repository = atom.project.getRepositories()[0] + const repository = atom.project.getRepositories()[0].async waitsForPromise(() => repository.refreshStatus()) }) @@ -389,13 +392,13 @@ describe('GitRepositoryAsync-js', () => { project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) project2.deserialize(atom.project.serialize(), atom.deserializers) - let buffer = project2.getBuffers()[0] + const buffer = project2.getBuffers()[0] waitsFor(() => buffer.loaded) runs(() => { buffer.append('changes') - let statusHandler = jasmine.createSpy('statusHandler') + const statusHandler = jasmine.createSpy('statusHandler') project2.getRepositories()[0].async.onDidChangeStatus(statusHandler) buffer.save() @@ -418,20 +421,20 @@ describe('GitRepositoryAsync-js', () => { // This is a change in implementation from the git-utils version it('just returns path if workdir is not provided', () => { - let _path = '/foo/bar/baz.txt' - let relPath = repository.relativize(_path) + const _path = '/foo/bar/baz.txt' + const relPath = repository.relativize(_path) expect(_path).toEqual(relPath) }) it('relativizes a repo path', () => { - let workdir = '/tmp/foo/bar/baz/' - let relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) + const workdir = '/tmp/foo/bar/baz/' + const relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) it("doesn't require workdir to end in a slash", () => { - let workdir = '/tmp/foo/bar/baz' - let relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) + const workdir = '/tmp/foo/bar/baz' + const relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) }) From 5fa8d6d5dbfaa60a1d82da867d60a5079b853482 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 16:45:09 -0500 Subject: [PATCH 101/254] Update the overrides. --- build/tasks/license-overrides.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index 173e41144..19f32fb84 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -124,10 +124,12 @@ module.exports = THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ 'tweetnacl@0.13.2': - license: 'CC0-1.0' + repository: 'https://github.com/dchest/tweetnacl-js' + license: 'Public Domain' source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.' 'json-schema@0.2.2': - license: 'AFLv2.1' + repository: 'https://github.com/kriszyp/json-schema' + license: 'BSD' source: 'README links to https://github.com/dojo/dojo/blob/8b6a5e4c42f9cf777dd39eaae8b188e0ebb59a4c/LICENSE' sourceText: """ Dojo is available under *either* the terms of the modified BSD license *or* the From ca2fc8dbb0c86ec91df152792aa3f1efbc55912b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 17:39:58 -0500 Subject: [PATCH 102/254] Make sure we're testing the right thing. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e6bdd801e..c0540f7b7 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -336,7 +336,7 @@ describe('GitRepositoryAsync-js', () => { buffer.reload() waitsFor(() => reloadHandler.callCount > 0) - runs(() => expect(statusHandler.callCount).toBe(1)) + runs(() => expect(reloadHandler.callCount).toBe(1)) }) }) From 36604141b91ab585cce1935898451f134cc4a2d9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 17:41:51 -0500 Subject: [PATCH 103/254] Let's use a separate method for side-effecting. --- src/git-repository-async.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b6ac9c08a..4f74cf620 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -83,7 +83,7 @@ module.exports = class GitRepositoryAsync { checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }) - .then(() => this.getPathStatus(_path)) + .then(() => this.refreshStatusForPath(_path)) } checkoutHeadForEditor (editor) { @@ -100,9 +100,17 @@ module.exports = class GitRepositoryAsync { }).then(filePath => this.checkoutHead(filePath)) } - // Returns a Promise that resolves to the status bit of a given path if it has - // one, otherwise 'current'. - getPathStatus (_path) { + // Refresh the status bit for the given path. + // + // Note that if the status of the path has changed, this will emit a + // 'did-change-status' event. + // + // path :: String + // The path whose status should be refreshed. + // + // Returns :: Promise + // The refreshed status bit for the path. + refreshStatusForPath (_path) { let relativePath return this.repoPromise .then(repo => { @@ -112,14 +120,21 @@ module.exports = class GitRepositoryAsync { .then(statuses => { const cachedStatus = this.pathStatusCache[relativePath] || 0 const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus && this.emitter != null) { + if (status !== cachedStatus) { + this.pathStatusCache[relativePath] = status this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } - this.pathStatusCache[relativePath] = status + return status }) } + // Returns a Promise that resolves to the status bit of a given path if it has + // one, otherwise 'current'. + getPathStatus (_path) { + return this.refreshStatusForPath(_path) + } + // Get the status of a directory in the repository's working directory. // // * `directoryPath` The {String} path to check. @@ -192,9 +207,7 @@ module.exports = class GitRepositoryAsync { const getBufferPathStatus = () => { const _path = buffer.getPath() if (_path) { - // We don't need to do anything with this promise, we just want the - // emitted event side effect - this.getPathStatus(_path) + this.refreshStatusForPath(_path) } } From db7a3063e94ee146b52f3a5cb12a3507dfd83ea6 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 11:22:40 -0500 Subject: [PATCH 104/254] Call destroy on the synchronous repo. --- spec/git-repository-async-spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index c0540f7b7..7611bfc55 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -366,7 +366,8 @@ describe('GitRepositoryAsync-js', () => { it('stops listening to the buffer when the repository is destroyed (regression)', async () => { const editor = await atom.workspace.open('other.txt') - repository.destroy() + const repo = atom.project.getRepositories()[0] + repo.destroy() expect(() => editor.save()).not.toThrow() }) }) From c9813d80259e77835e5038b074f6d5472a27c099 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 11:40:16 -0500 Subject: [PATCH 105/254] Ensure the repo is destroyed properly after testing. --- spec/git-repository-async-spec.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 7611bfc55..37d5cb898 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -413,29 +413,27 @@ describe('GitRepositoryAsync-js', () => { }) describe('GitRepositoryAsync::relativize(filePath, workdir)', () => { - let repository - beforeEach(() => { - atom.project.setPaths([copyRepository()]) - repository = atom.project.getRepositories()[0].async + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) }) // This is a change in implementation from the git-utils version it('just returns path if workdir is not provided', () => { const _path = '/foo/bar/baz.txt' - const relPath = repository.relativize(_path) + const relPath = repo.relativize(_path) expect(_path).toEqual(relPath) }) it('relativizes a repo path', () => { const workdir = '/tmp/foo/bar/baz/' - const relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) + const relativizedPath = repo.relativize(`${workdir}a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) it("doesn't require workdir to end in a slash", () => { const workdir = '/tmp/foo/bar/baz' - const relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) + const relativizedPath = repo.relativize(`${workdir}/a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) }) From 8a6ab81325850c5eda5e555eae351f8dfa1c0d71 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 13:26:06 -0500 Subject: [PATCH 106/254] 100% less racy. --- spec/git-repository-async-spec.js | 22 ++++++++-------------- src/git-repository-async.js | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 37d5cb898..829c33fcd 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -296,11 +296,9 @@ describe('GitRepositoryAsync-js', () => { // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that - // we're in a known state. *But* it's really hard to observe that from the - // outside in a non-racy fashion. So let's refresh again and wait for it - // to complete before we continue. + // we're in a known state. repository = atom.project.getRepositories()[0].async - waitsForPromise(() => repository.refreshStatus()) + waitsFor(() => !repository._isRefreshing()) }) it('emits a status-changed event when a buffer is saved', async () => { @@ -329,14 +327,6 @@ describe('GitRepositoryAsync-js', () => { runs(() => { expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - - const buffer = editor.getBuffer() - const reloadHandler = jasmine.createSpy('reloadHandler') - buffer.onDidReload(reloadHandler) - buffer.reload() - - waitsFor(() => reloadHandler.callCount > 0) - runs(() => expect(reloadHandler.callCount).toBe(1)) }) }) @@ -381,7 +371,7 @@ describe('GitRepositoryAsync-js', () => { // See the comment in the 'buffer events' beforeEach for why we need to do // this. const repository = atom.project.getRepositories()[0].async - waitsForPromise(() => repository.refreshStatus()) + waitsFor(() => !repository._isRefreshing()) }) afterEach(() => { @@ -393,6 +383,10 @@ describe('GitRepositoryAsync-js', () => { project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) project2.deserialize(atom.project.serialize(), atom.deserializers) + + const repo = project2.getRepositories()[0].async + waitsFor(() => !repo._isRefreshing()) + const buffer = project2.getBuffers()[0] waitsFor(() => buffer.loaded) @@ -400,7 +394,7 @@ describe('GitRepositoryAsync-js', () => { buffer.append('changes') const statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].async.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) buffer.save() waitsFor(() => statusHandler.callCount > 0) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4f74cf620..3475684a1 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -30,6 +30,7 @@ module.exports = class GitRepositoryAsync { this.pathStatusCache = {} this.repoPromise = Git.Repository.open(path) this.isCaseInsensitive = fs.isCaseInsensitive() + this._refreshingCount = 0 const {project} = options this.project = project @@ -174,15 +175,21 @@ module.exports = class GitRepositoryAsync { .then(branchRef => this.branch = branchRef) } - // Refreshes the git status. Note: the sync GitRepository class does this with - // a separate process, let's see if we can avoid that. + // Refreshes the git status. + // + // Returns :: Promise + // Resolves when refresh has completed. refreshStatus () { + this._refreshingCount++ + // TODO add upstream, branch, and submodule tracking const status = this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { + console.log(Object.keys(statuses)) // update the status cache - return Promise.all(statuses.map(status => [status.path(), status.statusBit()])) + const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) + return Promise.all(statusPairs) .then(statusesByPath => _.object(statusesByPath)) }) .then(newPathStatusCache => { @@ -195,12 +202,16 @@ module.exports = class GitRepositoryAsync { const branch = this._refreshBranch() - return Promise.all([status, branch]) + return Promise.all([status, branch]).then(_ => this._refreshingCount--) } // Section: Private // ================ + _isRefreshing () { + return this._refreshingCount === 0 + } + subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() From aa31c6c96fce96734e71f05e8e6bfe214873a38c Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 13:54:53 -0500 Subject: [PATCH 107/254] Don't log anymore. --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 3475684a1..00b325268 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -186,7 +186,6 @@ module.exports = class GitRepositoryAsync { const status = this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { - console.log(Object.keys(statuses)) // update the status cache const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) return Promise.all(statusPairs) From 38cf3b8f649a051ab8afb149ae8246ebcaf01d82 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 13:55:10 -0500 Subject: [PATCH 108/254] Use refreshStatusForBuffer. --- src/git-repository-async.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 00b325268..8e942ff1c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -36,11 +36,8 @@ module.exports = class GitRepositoryAsync { this.project = project if (this.project) { - this.subscriptions.add(this.project.onDidAddBuffer(buffer => { - this.subscribeToBuffer(buffer) - })) - - this.project.getBuffers().forEach(buffer => { this.subscribeToBuffer(buffer) }) + this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) + this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) } } @@ -214,7 +211,7 @@ module.exports = class GitRepositoryAsync { subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() - const getBufferPathStatus = () => { + const refreshStatusForBuffer = () => { const _path = buffer.getPath() if (_path) { this.refreshStatusForPath(_path) @@ -222,9 +219,9 @@ module.exports = class GitRepositoryAsync { } bufferSubscriptions.add( - buffer.onDidSave(getBufferPathStatus), - buffer.onDidReload(getBufferPathStatus), - buffer.onDidChangePath(getBufferPathStatus), + buffer.onDidSave(refreshStatusForBuffer), + buffer.onDidReload(refreshStatusForBuffer), + buffer.onDidChangePath(refreshStatusForBuffer), buffer.onDidDestroy(() => { bufferSubscriptions.dispose() this.subscriptions.remove(bufferSubscriptions) From 11fb5080397862e7ee54c887162dff1ada855230 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:22:57 -0500 Subject: [PATCH 109/254] :fire: all the disabled tests. --- spec/git-repository-async-spec.js | 35 ------------------------------- 1 file changed, 35 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 829c33fcd..19ee38c80 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -47,8 +47,6 @@ describe('GitRepositoryAsync-js', () => { }) describe('.getPath()', () => { - xit('returns the repository path for a .git directory path') - it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') const repoPath = await repo.getPath() @@ -181,39 +179,6 @@ describe('GitRepositoryAsync-js', () => { }) }) - xdescribe('.checkoutHeadForEditor(editor)', () => { - let filePath, editor - - beforeEach(() => { - const workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - waitsForPromise(() => atom.workspace.open(filePath)) - runs(() => editor = atom.workspace.getActiveTextEditor()) - }) - - xit('displays a confirmation dialog by default', () => { - spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) // eslint-disable-line - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - waitsForPromise(() => repo.checkoutHeadForEditor(editor)) - runs(() => expect(fs.readFileSync(filePath, 'utf8')).toBe('')) - }) - - xit('does not display a dialog when confirmation is disabled', () => { - spyOn(atom, 'confirm') - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - waitsForPromise(() => repo.checkoutHeadForEditor(editor)) - runs(() => { - expect(fs.readFileSync(filePath, 'utf8')).toBe('') - expect(atom.confirm).not.toHaveBeenCalled() - }) - }) - }) - describe('.getPathStatus(path)', () => { let filePath From fb7f2cce9531a70aac3264afa97661819f801298 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:23:11 -0500 Subject: [PATCH 110/254] These may be called more than once and that's ok. --- spec/git-repository-async-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 19ee38c80..941c1dce3 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -290,7 +290,7 @@ describe('GitRepositoryAsync-js', () => { waitsFor(() => statusHandler.callCount > 0) runs(() => { - expect(statusHandler.callCount).toBe(1) + expect(statusHandler.callCount).toBeGreaterThan(0) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) }) }) @@ -306,7 +306,7 @@ describe('GitRepositoryAsync-js', () => { waitsFor(() => statusHandler.callCount > 0) runs(() => { - expect(statusHandler.callCount).toBe(1) + expect(statusHandler.callCount).toBeGreaterThan(0) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) const pathHandler = jasmine.createSpy('pathHandler') @@ -315,7 +315,7 @@ describe('GitRepositoryAsync-js', () => { buffer.emitter.emit('did-change-path') waitsFor(() => pathHandler.callCount > 0) - runs(() => expect(pathHandler.callCount).toBe(1)) + runs(() => expect(pathHandler.callCount).toBeGreaterThan(0)) }) }) From e8a5864707e0f898e08d97e64acc614dd0fa2025 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:23:27 -0500 Subject: [PATCH 111/254] Do the work after waiting. --- spec/git-repository-async-spec.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 941c1dce3..b83cc4b83 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -351,21 +351,22 @@ describe('GitRepositoryAsync-js', () => { const repo = project2.getRepositories()[0].async waitsFor(() => !repo._isRefreshing()) - - const buffer = project2.getBuffers()[0] - - waitsFor(() => buffer.loaded) runs(() => { - buffer.append('changes') + const buffer = project2.getBuffers()[0] - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - buffer.save() - - waitsFor(() => statusHandler.callCount > 0) + waitsFor(() => buffer.loaded) runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + buffer.append('changes') + + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + buffer.save() + + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBeGreaterThan(0) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + }) }) }) }) From 6c58550bf4043c0a98fccce4d819a1215282a18a Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:36:25 -0500 Subject: [PATCH 112/254] Of course it's JS. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index b83cc4b83..dc8ae0107 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -23,7 +23,7 @@ function copyRepository () { return fs.realpathSync(workingDirPath) } -describe('GitRepositoryAsync-js', () => { +describe('GitRepositoryAsync', () => { let repo afterEach(() => { From d0b148a97a5d3daf65c074491feee3a50e32727d Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:39:00 -0500 Subject: [PATCH 113/254] Call the better-named method. --- src/git-repository.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 9ff0771b3..30cce92d0 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -324,7 +324,7 @@ class GitRepository # {::isStatusModified} or {::isStatusNew} to get more information. getPathStatus: (path) -> # Trigger events emitted on the async repo as well - @async.getPathStatus(path) + @async.refreshStatusForPath(path) repo = @getRepo(path) relativePath = @relativize(path) From bb14169e75675f374ac16fd804384f567ff19893 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:39:16 -0500 Subject: [PATCH 114/254] Take refreshStatusForPath into account for _refreshingCount. --- src/git-repository-async.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8e942ff1c..7f173b737 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -109,6 +109,8 @@ module.exports = class GitRepositoryAsync { // Returns :: Promise // The refreshed status bit for the path. refreshStatusForPath (_path) { + this._refreshingCount++ + let relativePath return this.repoPromise .then(repo => { @@ -125,6 +127,7 @@ module.exports = class GitRepositoryAsync { return status }) + .then(_ => this._refreshingCount--) } // Returns a Promise that resolves to the status bit of a given path if it has From 3ca4448afc784bc3ab5fc94692613e67fa22891e Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:53:57 -0500 Subject: [PATCH 115/254] Use the ES6 export syntax. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 7f173b737..a9f365e13 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -13,7 +13,7 @@ const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_M // Just using this for _.isEqual and _.object, we should impl our own here import _ from 'underscore-plus' -module.exports = class GitRepositoryAsync { +export default class GitRepositoryAsync { static open (path, options = {}) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? return new GitRepositoryAsync(path, options) From 428797c393a20f7c10001e001ff2b78a2a1a1de4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:59:09 -0500 Subject: [PATCH 116/254] Add back window refreshing. --- src/git-repository-async.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a9f365e13..38e098b6a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,7 +3,7 @@ import fs from 'fs-plus' import Git from 'nodegit' import path from 'path' -import {Emitter, CompositeDisposable} from 'event-kit' +import {Emitter, CompositeDisposable, Disposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -32,9 +32,15 @@ export default class GitRepositoryAsync { this.isCaseInsensitive = fs.isCaseInsensitive() this._refreshingCount = 0 + let {refreshOnWindowFocus} = options || true + if (refreshOnWindowFocus) { + const onWindowFocus = () => this.refreshStatus() + window.addEventListener('focus', onWindowFocus) + this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) + } + const {project} = options this.project = project - if (this.project) { this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) From fe4d3601d55f487b71a601b22446187e88a9fa8a Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 15:28:09 -0500 Subject: [PATCH 117/254] Organize similarly to git-repository.coffee so we can more easily tell what we're missing. --- src/git-repository-async.js | 447 ++++++++++++++++++++++++------------ 1 file changed, 294 insertions(+), 153 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 38e098b6a..77c07013a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -59,14 +59,209 @@ export default class GitRepositoryAsync { } } + // Event subscription + // ================== + + onDidDestroy (callback) { + return this.emitter.on('did-destroy', callback) + } + + onDidChangeStatus (callback) { + return this.emitter.on('did-change-status', callback) + } + + onDidChangeStatuses (callback) { + return this.emitter.on('did-change-statuses', callback) + } + + // Repository details + // ================== + + // Public: A {String} indicating the type of version control system used by + // this repository. + // + // Returns `"git"`. + getType () { + return 'git' + } + + // Public: Returns a {Promise} which resolves to the {String} path of the + // repository. getPath () { return this.repoPromise.then(repo => repo.path().replace(/\/$/, '')) } - isPathIgnored (_path) { - return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + // Public: Returns a {Promise} which resolves to the {String} working + // directory path of the repository. + getWorkingDirectory () { + throw new Error('Unimplemented') } + // Public: Returns a {Promise} that resolves to true if at the root, false if + // in a subfolder of the repository. + isProjectAtRoot () { + if (!this.projectAtRoot && this.project) { + this.projectAtRoot = Promise.resolve(() => { + return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) + }) + } + + return this.projectAtRoot + } + + // Public: Makes a path relative to the repository's working directory. + relativize (_path, workingDirectory) { + // Cargo-culted from git-utils. The original implementation also handles + // this.openedWorkingDirectory, which is set by git-utils when the + // repository is opened. Those branches of the if tree aren't included here + // yet, but if we determine we still need that here it should be simple to + // port. + // + // The original implementation also handled null workingDirectory as it + // pulled it from a sync function that could return null. We require it + // to be passed here. + if (!_path || !workingDirectory) { + return _path + } + + if (process.platform === 'win32') { + _path = _path.replace(/\\/g, '/') + } else { + if (_path[0] !== '/') { + return _path + } + } + + if (!/\/$/.test(workingDirectory)) { + workingDirectory = `${workingDirectory}/` + } + + if (this.isCaseInsensitive) { + const lowerCasePath = _path.toLowerCase() + + workingDirectory = workingDirectory.toLowerCase() + if (lowerCasePath.indexOf(workingDirectory) === 0) { + return _path.substring(workingDirectory.length) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + + return _path + } + + // Public: Returns true if the given branch exists. + hasBranch (branch) { + throw new Error('Unimplemented') + } + + // Public: Retrieves a shortened version of the HEAD reference value. + // + // This removes the leading segments of `refs/heads`, `refs/tags`, or + // `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 + // characters. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository contains submodules. + // + // Returns a {String}. + getShortHead (path) { + throw new Error('Unimplemented') + } + + // Public: Is the given path a submodule in the repository? + // + // * `path` The {String} path to check. + // + // Returns a {Promise} that resolves true if the given path is a submodule in + // the repository. + isSubmodule (_path) { + return this.repoPromise + .then(repo => repo.openIndex()) + .then(index => { + const entry = index.getByPath(_path) + const submoduleMode = 57344 // TODO compose this from libgit2 constants + return entry.mode === submoduleMode + }) + } + + // Public: Returns the number of commits behind the current branch is from the + // its upstream remote branch. + // + // * `reference` The {String} branch reference name. + // * `path` The {String} path in the repository to get this information for, + // only needed if the repository contains submodules. + getAheadBehindCount (reference, path) { + throw new Error('Unimplemented') + } + + // Public: Get the cached ahead/behind commit counts for the current branch's + // upstream branch. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `ahead` The {Number} of commits ahead. + // * `behind` The {Number} of commits behind. + getCachedUpstreamAheadBehindCount (path) { + throw new Error('Unimplemented') + } + + // Public: Returns the git configuration value specified by the key. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getConfigValue (key, path) { + throw new Error('Unimplemented') + } + + // Public: Returns the origin url of the repository. + // + // * `path` (optional) {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getOriginURL (path) { + throw new Error('Unimplemented') + } + + // Public: Returns the upstream branch for the current HEAD, or null if there + // is no upstream branch for the current HEAD. + // + // * `path` An optional {String} path in the repo to get this information for, + // only needed if the repository contains submodules. + // + // Returns a {String} branch name such as `refs/remotes/origin/master`. + getUpstreamBranch (path) { + throw new Error('Unimplemented') + } + + // Public: Gets all the local and remote references. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `heads` An {Array} of head reference names. + // * `remotes` An {Array} of remote reference names. + // * `tags` An {Array} of tag reference names. + getReferences (path) { + throw new Error('Unimplemented') + } + + // Public: Returns the current {String} SHA for the given reference. + // + // * `reference` The {String} reference to get the target of. + // * `path` An optional {String} path in the repo to get the reference target + // for. Only needed if the repository contains submodules. + getReferenceTarget (reference, path) { + throw new Error('Unimplemented') + } + + // Reading Status + // ============== + isPathModified (_path) { return this._filterStatusesByPath(_path).then(statuses => { return statuses.filter(status => status.isModified()).length > 0 @@ -79,29 +274,36 @@ export default class GitRepositoryAsync { }) } - checkoutHead (_path) { - return this.repoPromise - .then(repo => { - const checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, repo.workdir())] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) - }) - .then(() => this.refreshStatusForPath(_path)) + isPathIgnored (_path) { + return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) } - checkoutHeadForEditor (editor) { - return new Promise((resolve, reject) => { - const filePath = editor.getPath() - if (filePath) { - if (editor.buffer.isModified()) { - editor.buffer.reload() - } - resolve(filePath) - } else { - reject() - } - }).then(filePath => this.checkoutHead(filePath)) + // Get the status of a directory in the repository's working directory. + // + // * `directoryPath` The {String} path to check. + // + // Returns a promise resolving to a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + + getDirectoryStatus (directoryPath) { + let relativePath + // XXX _filterSBD already gets repoPromise + return this.repoPromise + .then(repo => { + relativePath = this.relativize(directoryPath, repo.workdir()) + return this._filterStatusesByDirectory(relativePath) + }) + .then(statuses => { + return Promise.all(statuses.map(s => s.statusBit())).then(bits => { + let directoryStatus = 0 + const filteredBits = bits.filter(b => b > 0) + if (filteredBits.length > 0) { + filteredBits.forEach(bit => directoryStatus |= bit) + } + + return directoryStatus + }) + }) } // Refresh the status bit for the given path. @@ -142,34 +344,83 @@ export default class GitRepositoryAsync { return this.refreshStatusForPath(_path) } - // Get the status of a directory in the repository's working directory. + // Public: Get the cached status for the given path. // - // * `directoryPath` The {String} path to check. + // * `path` A {String} path in the repository, relative or absolute. // - // Returns a promise resolving to a {Number} representing the status. This value can be passed to - // {::isStatusModified} or {::isStatusNew} to get more information. + // Returns a {Promise} which resolves to a status {Number} or null if the + // path is not in the cache. + getCachedPathStatus (_path) { + return this.repoPromise + .then(repo => this.relativize(_path, repo.workdir())) + .then(relativePath => this.pathStatusCache[relativePath]) + } - getDirectoryStatus (directoryPath) { - let relativePath - // XXX _filterSBD already gets repoPromise + isStatusNew (statusBit) { + return (statusBit & newStatusFlags) > 0 + } + + isStatusModified (statusBit) { + return (statusBit & modifiedStatusFlags) > 0 + } + + isStatusStaged (statusBit) { + return (statusBit & indexStatusFlags) > 0 + } + + isStatusIgnored (statusBit) { + return (statusBit & (1 << 14)) > 0 + } + + isStatusDeleted (statusBit) { + return (statusBit & deletedStatusFlags) > 0 + } + + // Checking Out + // ============ + + // Public: Restore the contents of a path in the working directory and index + // to the version at `HEAD`. + // + // This is essentially the same as running: + // + // ```sh + // git reset HEAD -- + // git checkout HEAD -- + // ``` + // + // * `path` The {String} path to checkout. + // + // Returns a {Promise} that resolves or rejects depending on whether the + // method was successful. + checkoutHead (_path) { return this.repoPromise .then(repo => { - relativePath = this.relativize(directoryPath, repo.workdir()) - return this._filterStatusesByDirectory(relativePath) - }) - .then(statuses => { - return Promise.all(statuses.map(s => s.statusBit())).then(bits => { - let directoryStatus = 0 - const filteredBits = bits.filter(b => b > 0) - if (filteredBits.length > 0) { - filteredBits.forEach(bit => directoryStatus |= bit) - } - - return directoryStatus - }) + const checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + return Git.Checkout.head(repo, checkoutOptions) }) + .then(() => this.refreshStatusForPath(_path)) } + checkoutHeadForEditor (editor) { + return new Promise((resolve, reject) => { + const filePath = editor.getPath() + if (filePath) { + if (editor.buffer.isModified()) { + editor.buffer.reload() + } + resolve(filePath) + } else { + reject() + } + }).then(filePath => this.checkoutHead(filePath)) + } + + // Private + // ======= + // Get the current branch and update this.branch. // // Returns :: Promise @@ -241,74 +492,6 @@ export default class GitRepositoryAsync { return } - relativize (_path, workingDirectory) { - // Cargo-culted from git-utils. The original implementation also handles - // this.openedWorkingDirectory, which is set by git-utils when the - // repository is opened. Those branches of the if tree aren't included here - // yet, but if we determine we still need that here it should be simple to - // port. - // - // The original implementation also handled null workingDirectory as it - // pulled it from a sync function that could return null. We require it - // to be passed here. - if (!_path || !workingDirectory) { - return _path - } - - if (process.platform === 'win32') { - _path = _path.replace(/\\/g, '/') - } else { - if (_path[0] !== '/') { - return _path - } - } - - if (!/\/$/.test(workingDirectory)) { - workingDirectory = `${workingDirectory}/` - } - - if (this.isCaseInsensitive) { - const lowerCasePath = _path.toLowerCase() - - workingDirectory = workingDirectory.toLowerCase() - if (lowerCasePath.indexOf(workingDirectory) === 0) { - return _path.substring(workingDirectory.length) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } - } - - return _path - } - - getCachedPathStatus (_path) { - return this.repoPromise.then(repo => { - return this.pathStatusCache[this.relativize(_path, repo.workdir())] - }) - } - - isStatusNew (statusBit) { - return (statusBit & newStatusFlags) > 0 - } - - isStatusModified (statusBit) { - return (statusBit & modifiedStatusFlags) > 0 - } - - isStatusStaged (statusBit) { - return (statusBit & indexStatusFlags) > 0 - } - - isStatusIgnored (statusBit) { - return (statusBit & (1 << 14)) > 0 - } - - isStatusDeleted (statusBit) { - return (statusBit & deletedStatusFlags) > 0 - } - _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this let basePath = null @@ -329,46 +512,4 @@ export default class GitRepositoryAsync { return statuses.filter(status => status.path().indexOf(directoryPath) === 0) }) } - // Event subscription - // ================== - - onDidChangeStatus (callback) { - return this.emitter.on('did-change-status', callback) - } - - onDidChangeStatuses (callback) { - return this.emitter.on('did-change-statuses', callback) - } - - onDidDestroy (callback) { - return this.emitter.on('did-destroy', callback) - } - - // - // Section: Repository Details - // - - // Returns a {Promise} that resolves true if at the root, false if in a - // subfolder of the repository. - isProjectAtRoot () { - if (this.projectAtRoot === undefined) { - this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) - }) - } - - return this.projectAtRoot - } - - // Returns a {Promise} that resolves true if the given path is a submodule in - // the repository. - isSubmodule (_path) { - return this.repoPromise - .then(repo => repo.openIndex()) - .then(index => { - const entry = index.getByPath(_path) - const submoduleMode = 57344 // TODO compose this from libgit2 constants - return entry.mode === submoduleMode - }) - } } From aa634570e6371ee529dfb4ecc48a3e0795e03999 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 16:16:30 -0500 Subject: [PATCH 118/254] Added getShortHead --- spec/git-repository-async-spec.js | 12 ++++++++++++ src/git-repository-async.js | 21 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index dc8ae0107..99162920f 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -397,4 +397,16 @@ describe('GitRepositoryAsync', () => { expect(relativizedPath).toBe('a/b.txt') }) }) + + describe('.getShortHead(path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the human-readable branch name', async () => { + const head = await repo.getShortHead() + expect(head).toBe('master') + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 77c07013a..283b0a77d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -166,9 +166,11 @@ export default class GitRepositoryAsync { // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository contains submodules. // - // Returns a {String}. - getShortHead (path) { - throw new Error('Unimplemented') + // Returns a {Promise} which resolves to a {String}. + getShortHead (_path) { + return this._getRepo(_path) + .then(repo => repo.getCurrentBranch()) + .then(branch => branch.shorthand()) } // Public: Is the given path a submodule in the repository? @@ -468,6 +470,19 @@ export default class GitRepositoryAsync { return this._refreshingCount === 0 } + _getRepo (_path) { + if (!_path) return this.repoPromise + + return this.isSubmodule(_path) + .then(isSubmodule => { + if (isSubmodule) { + return Git.Repository.open(_path) + } else { + return this.repoPromise + } + }) + } + subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() From cb62d917de57b953221c0d7002b2cb1e52c62b6e Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 17:20:38 -0500 Subject: [PATCH 119/254] Added getAheadBehindCount. --- spec/git-repository-async-spec.js | 27 ++++++++++++ src/git-repository-async.js | 69 ++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 99162920f..1b45d12f6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -409,4 +409,31 @@ describe('GitRepositoryAsync', () => { expect(head).toBe('master') }) }) + + describe('.getAheadBehindCount(reference, path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns 0, 0 for a branch with no upstream', async () => { + const {ahead, behind} = await repo.getAheadBehindCount('master') + expect(ahead).toBe(0) + expect(behind).toBe(0) + }) + }) + + describe('.getCachedUpstreamAheadBehindCount(path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns 0, 0 for a branch with no upstream', async () => { + await repo.refreshStatus() + const {ahead, behind} = repo.getCachedUpstreamAheadBehindCount() + expect(ahead).toBe(0) + expect(behind).toBe(0) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 283b0a77d..91954fab6 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -23,13 +23,15 @@ export default class GitRepositoryAsync { return Git } - constructor (path, options) { + constructor (_path, options) { this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.open(path) + this.repoPromise = Git.Repository.open(_path) this.isCaseInsensitive = fs.isCaseInsensitive() + this.upstreamByPath = {} + this._refreshingCount = 0 let {refreshOnWindowFocus} = options || true @@ -193,10 +195,21 @@ export default class GitRepositoryAsync { // its upstream remote branch. // // * `reference` The {String} branch reference name. - // * `path` The {String} path in the repository to get this information for, - // only needed if the repository contains submodules. - getAheadBehindCount (reference, path) { - throw new Error('Unimplemented') + // * `path` The {String} path in the repository to get this information + // for, only needed if the repository contains submodules. + // + // Returns a {Promise} which resolves to an {Object} with the following keys: + // * `ahead` The {Number} of commits ahead. + // * `behind` The {Number} of commits behind. + getAheadBehindCount (reference, _path) { + return this._getRepo(_path) + .then(repo => Promise.all([repo, repo.getBranch(reference)])) + .then(([repo, local]) => Promise.all([repo, local, Git.Branch.upstream(local)])) + .then(([repo, local, upstream]) => { + if (!upstream) return {ahead: 0, behind: 0} + + return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) + }) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -208,15 +221,15 @@ export default class GitRepositoryAsync { // Returns an {Object} with the following keys: // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount (path) { - throw new Error('Unimplemented') + getCachedUpstreamAheadBehindCount (_path) { + return this.upstreamByPath[_path || '.'] } // Public: Returns the git configuration value specified by the key. // // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository has submodules. - getConfigValue (key, path) { + getConfigValue (key, _path) { throw new Error('Unimplemented') } @@ -224,7 +237,7 @@ export default class GitRepositoryAsync { // // * `path` (optional) {String} path in the repository to get this information // for, only needed if the repository has submodules. - getOriginURL (path) { + getOriginURL (_path) { throw new Error('Unimplemented') } @@ -235,7 +248,7 @@ export default class GitRepositoryAsync { // only needed if the repository contains submodules. // // Returns a {String} branch name such as `refs/remotes/origin/master`. - getUpstreamBranch (path) { + getUpstreamBranch (_path) { throw new Error('Unimplemented') } @@ -248,7 +261,7 @@ export default class GitRepositoryAsync { // * `heads` An {Array} of head reference names. // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. - getReferences (path) { + getReferences (_path) { throw new Error('Unimplemented') } @@ -257,7 +270,7 @@ export default class GitRepositoryAsync { // * `reference` The {String} reference to get the target of. // * `path` An optional {String} path in the repo to get the reference target // for. Only needed if the repository contains submodules. - getReferenceTarget (reference, path) { + getReferenceTarget (reference, _path) { throw new Error('Unimplemented') } @@ -431,18 +444,16 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => repo.getCurrentBranch()) .then(ref => ref.name()) - .then(branchRef => this.branch = branchRef) + .then(branchName => this.branch = branchName) } - // Refreshes the git status. - // - // Returns :: Promise - // Resolves when refresh has completed. - refreshStatus () { - this._refreshingCount++ + _refreshAheadBehindCount (branchName) { + return this.getAheadBehindCount(branchName) + .then(counts => this.upstreamByPath['.'] = counts) + } - // TODO add upstream, branch, and submodule tracking - const status = this.repoPromise + _refreshStatus () { + return this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { // update the status cache @@ -457,10 +468,22 @@ export default class GitRepositoryAsync { this.pathStatusCache = newPathStatusCache return newPathStatusCache }) + } + // Refreshes the git status. + // + // Returns :: Promise + // Resolves when refresh has completed. + refreshStatus () { + this._refreshingCount++ + + // TODO add submodule tracking + + const status = this._refreshStatus() const branch = this._refreshBranch() + const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName)) - return Promise.all([status, branch]).then(_ => this._refreshingCount--) + return Promise.all([status, branch, aheadBehind]).then(_ => this._refreshingCount--) } // Section: Private From 8da08724ad5e54ae19a15a359b62a070f3653b3e Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 23:51:33 -0500 Subject: [PATCH 120/254] Added getDiffStats. --- spec/git-repository-async-spec.js | 17 ++++++++++++++ src/git-repository-async.js | 38 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 1b45d12f6..6aa2e6f81 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -436,4 +436,21 @@ describe('GitRepositoryAsync', () => { expect(behind).toBe(0) }) }) + + describe('.getDiffStats(path)', () => { + let workingDirectory + beforeEach(() => { + workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the diff stat', async () => { + const filePath = path.join(workingDirectory, 'a.txt') + fs.writeFileSync(filePath, 'change') + + const {added, deleted} = await repo.getDiffStats('a.txt') + expect(added).toBe(1) + expect(deleted).toBe(0) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 91954fab6..131b1b51c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -391,6 +391,44 @@ export default class GitRepositoryAsync { return (statusBit & deletedStatusFlags) > 0 } + // Retrieving Diffs + // ================ + // Public: Retrieves the number of lines added and removed to a path. + // + // This compares the working directory contents of the path to the `HEAD` + // version. + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to an {Object} with the following keys: + // * `added` The {Number} of added lines. + // * `deleted` The {Number} of deleted lines. + getDiffStats (_path) { + return this.repoPromise + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) + .then(([repo, tree]) => { + const options = new Git.DiffOptions() + options.pathspec = _path + return Git.Diff.treeToWorkdir(repo, tree, options) + }) + .then(diff => diff.patches()) // :: Array + .then(patches => Promise.all(patches.map(p => p.hunks()))) // :: Array> + .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // :: Array> + .then(lines => { + const stats = {added: 0, deleted: 0} + for (const line of _.flatten(lines)) { + const origin = line.origin() + if (origin === Git.Diff.LINE.ADDITION) { + stats.added++ + } else if (origin === Git.Diff.LINE.DELETION) { + stats.deleted++ + } + } + return stats + }) + } + // Checking Out // ============ From a3a77180de58bdceeb78c452b1598e7ac9ff57cb Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 09:33:30 -0500 Subject: [PATCH 121/254] More helpful type annotations. --- src/git-repository-async.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 131b1b51c..705f98870 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -412,10 +412,10 @@ export default class GitRepositoryAsync { options.pathspec = _path return Git.Diff.treeToWorkdir(repo, tree, options) }) - .then(diff => diff.patches()) // :: Array - .then(patches => Promise.all(patches.map(p => p.hunks()))) // :: Array> - .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // :: Array> - .then(lines => { + .then(diff => diff.patches()) + .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array + .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // hunks :: Array> + .then(lines => { // lines :: Array> const stats = {added: 0, deleted: 0} for (const line of _.flatten(lines)) { const origin = line.origin() From 81dbc5c867cc88b7b01663656ce19890136cc16e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 09:48:38 -0500 Subject: [PATCH 122/254] Move the refreshingCount changes closer to where the work is done. --- src/git-repository-async.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 705f98870..614d18a26 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -350,7 +350,10 @@ export default class GitRepositoryAsync { return status }) - .then(_ => this._refreshingCount--) + .then(status => { + this._refreshingCount-- + return status + }) } // Returns a Promise that resolves to the status bit of a given path if it has @@ -491,6 +494,8 @@ export default class GitRepositoryAsync { } _refreshStatus () { + this._refreshingCount++ + return this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { @@ -506,22 +511,21 @@ export default class GitRepositoryAsync { this.pathStatusCache = newPathStatusCache return newPathStatusCache }) + .then(_ => this._refreshingCount--) } // Refreshes the git status. // - // Returns :: Promise + // Returns :: Promise // Resolves when refresh has completed. refreshStatus () { - this._refreshingCount++ - // TODO add submodule tracking const status = this._refreshStatus() const branch = this._refreshBranch() const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName)) - return Promise.all([status, branch, aheadBehind]).then(_ => this._refreshingCount--) + return Promise.all([status, branch, aheadBehind]).then(_ => null) } // Section: Private From 9bb8703978e9438bbc84bd15b3f76aeb45c06896 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 09:49:10 -0500 Subject: [PATCH 123/254] Don't make the async repo duplicate the work if it's wrapped by a sync repo. --- src/git-repository-async.js | 4 ++-- src/git-repository.coffee | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 614d18a26..2c86c129d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -41,9 +41,9 @@ export default class GitRepositoryAsync { this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) } - const {project} = options + const {project, subscribeToBuffers} = options this.project = project - if (this.project) { + if (this.project && subscribeToBuffers) { this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) } diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 30cce92d0..5a0df6743 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -76,7 +76,14 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - @async = GitRepositoryAsync.open(path, options) + asyncOptions = {} + for key, val of options + asyncOptions[key] = val + # GitRepository itself will handle these cases by manually calling through + # to the async repo. + asyncOptions.refreshOnWindowFocus = false + asyncOptions.subscribeToBuffers = false + @async = GitRepositoryAsync.open(path, asyncOptions) @statuses = {} @upstream = {ahead: 0, behind: 0} From 46d6e3b3c456ef0c98c3866be9805c55c5f9e7d9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:29:34 -0500 Subject: [PATCH 124/254] Added .hasBranch --- spec/git-repository-async-spec.js | 17 +++++++++++++++++ src/git-repository-async.js | 12 ++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 6aa2e6f81..ba4559225 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -453,4 +453,21 @@ describe('GitRepositoryAsync', () => { expect(deleted).toBe(0) }) }) + + describe('.hasBranch(branch)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('resolves true when the branch exists', async () => { + const hasBranch = await repo.hasBranch('master') + expect(hasBranch).toBe(true) + }) + + it("resolves false when the branch doesn't exist", async () => { + const hasBranch = await repo.hasBranch('trolleybus') + expect(hasBranch).toBe(false) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 2c86c129d..c6d85943a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -96,7 +96,7 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to the {String} working // directory path of the repository. getWorkingDirectory () { - throw new Error('Unimplemented') + return this.repoPromise.then(repo => repo.workdir()) } // Public: Returns a {Promise} that resolves to true if at the root, false if @@ -104,7 +104,7 @@ export default class GitRepositoryAsync { isProjectAtRoot () { if (!this.projectAtRoot && this.project) { this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) + return this.repoPromise.then(repo => this.project.relativize(repo.workdir())) }) } @@ -154,9 +154,13 @@ export default class GitRepositoryAsync { return _path } - // Public: Returns true if the given branch exists. + // Public: Returns a {Promise} which resolves to whether the given branch + // exists. hasBranch (branch) { - throw new Error('Unimplemented') + return this.repoPromise + .then(repo => repo.getBranch(branch)) + .then(branch => branch != null) + .catch(_ => false) } // Public: Retrieves a shortened version of the HEAD reference value. From b60056f960c6f33b1341d41eb0eeea5ea7aff441 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:34:05 -0500 Subject: [PATCH 125/254] Added .getUpstreamBranch --- src/git-repository-async.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index c6d85943a..8bdb8ca22 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -251,9 +251,12 @@ export default class GitRepositoryAsync { // * `path` An optional {String} path in the repo to get this information for, // only needed if the repository contains submodules. // - // Returns a {String} branch name such as `refs/remotes/origin/master`. + // Returns a {Promise} which resolves to a {String} branch name such as + // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.getCurrentBranch()) + .then(branch => Git.Branch.upstream(branch)) } // Public: Gets all the local and remote references. From c1e511927ba97d0c2d283223a073fdd0918e9ede Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:44:31 -0500 Subject: [PATCH 126/254] Added .getReferences --- spec/git-repository-async-spec.js | 15 +++++++++++++++ src/git-repository-async.js | 20 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index ba4559225..27306d9d9 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -470,4 +470,19 @@ describe('GitRepositoryAsync', () => { expect(hasBranch).toBe(false) }) }) + + describe('.getReferences(path)', () => { + let workingDirectory + beforeEach(() => { + workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the heads, remotes, and tags', async () => { + const {heads, remotes, tags} = await repo.getReferences() + expect(heads.length).toBe(1) + expect(remotes.length).toBe(0) + expect(tags.length).toBe(0) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8bdb8ca22..0dcc5b786 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -264,12 +264,28 @@ export default class GitRepositoryAsync { // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository has submodules. // - // Returns an {Object} with the following keys: + // Returns a {Promise} which resolves to an {Object} with the following keys: // * `heads` An {Array} of head reference names. // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) + .then(refs => { + const heads = [] + const remotes = [] + const tags = [] + for (const ref of refs) { + if (ref.isTag()) { + tags.push(ref.name()) + } else if (ref.isRemote()) { + remotes.push(ref.name()) + } else if (ref.isBranch()) { + heads.push(ref.name()) + } + } + return {heads, remotes, tags} + }) } // Public: Returns the current {String} SHA for the given reference. From 70601772d085aa90222ff874e226335c8a6889ca Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:54:55 -0500 Subject: [PATCH 127/254] Added .getReferenceTarget --- spec/git-repository-async-spec.js | 13 +++++++++++++ src/git-repository-async.js | 9 +++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 27306d9d9..8d0c87bd6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -485,4 +485,17 @@ describe('GitRepositoryAsync', () => { expect(tags.length).toBe(0) }) }) + + describe('.getReferenceTarget(reference, path)', () => { + let workingDirectory + beforeEach(() => { + workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the SHA target', async () => { + const SHA = await repo.getReferenceTarget('refs/heads/master') + expect(SHA).toBe('8a9c86f1cb1f14b8f436eb91f4b052c8802ca99e') + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 0dcc5b786..1fd90362d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -288,13 +288,18 @@ export default class GitRepositoryAsync { }) } - // Public: Returns the current {String} SHA for the given reference. + // Public: Get the SHA for the given reference. // // * `reference` The {String} reference to get the target of. // * `path` An optional {String} path in the repo to get the reference target // for. Only needed if the repository contains submodules. + // + // Returns a {Promise} which resolves to the current {String} SHA for the + // given reference. getReferenceTarget (reference, _path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.getReference(reference)) + .then(ref => ref.target().tostrS()) } // Reading Status From 74a0528bef18fdd4d9013012cb59db891009b400 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:18:00 -0500 Subject: [PATCH 128/254] Added .getConfigValue --- spec/git-repository-async-spec.js | 24 ++++++++++++++++++++---- src/git-repository-async.js | 8 +++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 8d0c87bd6..f226743b2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -472,9 +472,8 @@ describe('GitRepositoryAsync', () => { }) describe('.getReferences(path)', () => { - let workingDirectory beforeEach(() => { - workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) }) @@ -487,9 +486,8 @@ describe('GitRepositoryAsync', () => { }) describe('.getReferenceTarget(reference, path)', () => { - let workingDirectory beforeEach(() => { - workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) }) @@ -498,4 +496,22 @@ describe('GitRepositoryAsync', () => { expect(SHA).toBe('8a9c86f1cb1f14b8f436eb91f4b052c8802ca99e') }) }) + + describe('.getConfigValue(key, path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + console.log(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('looks up the value for the key', async () => { + const bare = await repo.getConfigValue('core.bare') + expect(bare).toBe('false') + }) + + it("resolves to null if there's no value", async () => { + const value = await repo.getConfigValue('my.special.key') + expect(value).toBe(null) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1fd90362d..432502d17 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -233,8 +233,14 @@ export default class GitRepositoryAsync { // // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository has submodules. + // + // Returns a {Promise} which resolves to the {String} git configuration value + // specified by the key. getConfigValue (key, _path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.configSnapshot()) + .then(config => config.getStringBuf(key)) + .catch(_ => null) } // Public: Returns the origin url of the repository. From df839537a95264c0354669fae6966e2e9298b08f Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:26:32 -0500 Subject: [PATCH 129/254] A lil more efficient .getReferenceTarget. --- src/git-repository-async.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 432502d17..e893bb9e5 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -304,8 +304,8 @@ export default class GitRepositoryAsync { // given reference. getReferenceTarget (reference, _path) { return this._getRepo(_path) - .then(repo => repo.getReference(reference)) - .then(ref => ref.target().tostrS()) + .then(repo => Git.Reference.nameToId(repo, reference)) + .then(oid => oid.tostrS()) } // Reading Status From d21a91a412ab24178c27eeff5eb234b897bc6bb8 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:26:45 -0500 Subject: [PATCH 130/254] Added .getOriginalURL. --- src/git-repository-async.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e893bb9e5..f65156856 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -243,12 +243,15 @@ export default class GitRepositoryAsync { .catch(_ => null) } - // Public: Returns the origin url of the repository. + // Public: Get the URL for the 'origin' remote. // // * `path` (optional) {String} path in the repository to get this information // for, only needed if the repository has submodules. + // + // Returns a {Promise} which resolves to the {String} origin url of the + // repository. getOriginURL (_path) { - throw new Error('Unimplemented') + return this.getConfigValue('remote.origin.url', _path) } // Public: Returns the upstream branch for the current HEAD, or null if there From c3adee6363f9dc6d17b9c5b70eb3080b57358d94 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:41:22 -0500 Subject: [PATCH 131/254] Add some more stubs/TODOs. --- src/git-repository-async.js | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f65156856..d1cd90cc4 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -189,6 +189,8 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => repo.openIndex()) .then(index => { + // TODO: This'll probably be wrong if the submodule doesn't exist in the + // index yet? Is that a thing? const entry = index.getByPath(_path) const submoduleMode = 57344 // TODO compose this from libgit2 constants return entry.mode === submoduleMode @@ -469,6 +471,21 @@ export default class GitRepositoryAsync { }) } + // Public: Retrieves the line diffs comparing the `HEAD` version of the given + // path and the given text. + // + // * `path` The {String} path relative to the repository. + // * `text` The {String} to compare against the `HEAD` contents + // + // Returns an {Array} of hunk {Object}s with the following keys: + // * `oldStart` The line {Number} of the old hunk. + // * `newStart` The line {Number} of the new hunk. + // * `oldLines` The {Number} of lines in the old hunk. + // * `newLines` The {Number} of lines in the new hunk + getLineDiffs (_path, text) { + throw new Error('Unimplemented') + } + // Checking Out // ============ @@ -497,6 +514,21 @@ export default class GitRepositoryAsync { .then(() => this.refreshStatusForPath(_path)) } + // Public: Checks out a branch in your repository. + // + // * `reference` The {String} reference to checkout. + // * `create` A {Boolean} value which, if true creates the new reference if + // it doesn't exist. + // + // Returns a Boolean that's true if the method was successful. + checkoutReference (reference, create) { + throw new Error('Unimplemented') + // @getRepo().checkoutReference(reference, create) + } + + // Private + // ======= + checkoutHeadForEditor (editor) { return new Promise((resolve, reject) => { const filePath = editor.getPath() @@ -511,9 +543,6 @@ export default class GitRepositoryAsync { }).then(filePath => this.checkoutHead(filePath)) } - // Private - // ======= - // Get the current branch and update this.branch. // // Returns :: Promise From 53be0cf9655d5e948941a3f2df4079092d0b7f76 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 14:07:12 -0500 Subject: [PATCH 132/254] Added .checkoutReference --- spec/git-repository-async-spec.js | 26 +++++++++++++++++++++++--- src/git-repository-async.js | 26 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index f226743b2..23acedff0 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -462,12 +462,12 @@ describe('GitRepositoryAsync', () => { it('resolves true when the branch exists', async () => { const hasBranch = await repo.hasBranch('master') - expect(hasBranch).toBe(true) + expect(hasBranch).toBeTruthy() }) it("resolves false when the branch doesn't exist", async () => { const hasBranch = await repo.hasBranch('trolleybus') - expect(hasBranch).toBe(false) + expect(hasBranch).toBeFalsy() }) }) @@ -511,7 +511,27 @@ describe('GitRepositoryAsync', () => { it("resolves to null if there's no value", async () => { const value = await repo.getConfigValue('my.special.key') - expect(value).toBe(null) + expect(value).toBeNull() + }) + }) + + describe('.checkoutReference(reference, create)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + console.log(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('can create new branches', () => { + let success = false + let threw = false + waitsForPromise(() => repo.checkoutReference('my-b', true) + .then(_ => success = true) + .catch(_ => threw = true)) + runs(() => { + expect(success).toBeTruthy() + expect(threw).toBeFalsy() + }) }) }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d1cd90cc4..275feb4c8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -483,6 +483,11 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { + // # Ignore eol of line differences on windows so that files checked in as + // # LF don't report every line modified when the text contains CRLF endings. + // options = ignoreEolWhitespace: process.platform is 'win32' + // repo = @getRepo(path) + // repo.getLineDiffs(repo.relativize(path), text, options) throw new Error('Unimplemented') } @@ -514,16 +519,31 @@ export default class GitRepositoryAsync { .then(() => this.refreshStatusForPath(_path)) } + _createBranch (name) { + return this.repoPromise + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, commit]) => repo.createBranch(name, commit)) + } + // Public: Checks out a branch in your repository. // // * `reference` The {String} reference to checkout. // * `create` A {Boolean} value which, if true creates the new reference if // it doesn't exist. // - // Returns a Boolean that's true if the method was successful. + // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - throw new Error('Unimplemented') - // @getRepo().checkoutReference(reference, create) + return this.repoPromise + .then(repo => repo.checkoutBranch(reference)) + .catch(error => { + if (create) { + return this._createBranch(reference) + .then(_ => this.checkoutReference(reference, false)) + } else { + throw error + } + }) + .then(_ => null) } // Private From 4fbbcac3bc5d524426b1dbb32fe7a0b3f3259eea Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 15:34:58 -0500 Subject: [PATCH 133/254] Added .getLineDiffs --- spec/git-repository-async-spec.js | 17 +++++++-- src/git-repository-async.js | 60 +++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 23acedff0..9aca3356c 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -500,7 +500,6 @@ describe('GitRepositoryAsync', () => { describe('.getConfigValue(key, path)', () => { beforeEach(() => { const workingDirectory = copyRepository() - console.log(workingDirectory) repo = GitRepositoryAsync.open(workingDirectory) }) @@ -518,7 +517,6 @@ describe('GitRepositoryAsync', () => { describe('.checkoutReference(reference, create)', () => { beforeEach(() => { const workingDirectory = copyRepository() - console.log(workingDirectory) repo = GitRepositoryAsync.open(workingDirectory) }) @@ -534,4 +532,19 @@ describe('GitRepositoryAsync', () => { }) }) }) + + describe('.getLineDiffs(path, text)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('can get the line diff', async () => { + const {oldStart, newStart, oldLines, newLines} = await repo.getLineDiffs('a.txt', 'hi there') + expect(oldStart).toBe(0) + expect(oldLines).toBe(0) + expect(newStart).toBe(1) + expect(newLines).toBe(1) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 275feb4c8..f0c394a02 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -433,6 +433,18 @@ export default class GitRepositoryAsync { return (statusBit & deletedStatusFlags) > 0 } + _getDiffHunks (diff) { + return diff.patches() + .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array + .then(hunks => _.flatten(hunks)) // hunks :: Array> + } + + _getDiffLines (diff) { + return this._getDiffHunks(diff) + .then(hunks => Promise.all(hunks.map(h => h.lines()))) + .then(lines => _.flatten(lines)) // lines :: Array> + } + // Retrieving Diffs // ================ // Public: Retrieves the number of lines added and removed to a path. @@ -454,12 +466,10 @@ export default class GitRepositoryAsync { options.pathspec = _path return Git.Diff.treeToWorkdir(repo, tree, options) }) - .then(diff => diff.patches()) - .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array - .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // hunks :: Array> - .then(lines => { // lines :: Array> + .then(diff => this._getDiffLines(diff)) + .then(lines => { const stats = {added: 0, deleted: 0} - for (const line of _.flatten(lines)) { + for (const line of lines) { const origin = line.origin() if (origin === Git.Diff.LINE.ADDITION) { stats.added++ @@ -471,6 +481,14 @@ export default class GitRepositoryAsync { }) } + _diffBlobToBuffer (blob, buffer, options) { + const hunks = [] + const hunkCallback = (delta, hunk, payload) => { + hunks.push(hunk) + } + return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null).then(_ => hunks) + } + // Public: Retrieves the line diffs comparing the `HEAD` version of the given // path and the given text. // @@ -483,12 +501,32 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { - // # Ignore eol of line differences on windows so that files checked in as - // # LF don't report every line modified when the text contains CRLF endings. - // options = ignoreEolWhitespace: process.platform is 'win32' - // repo = @getRepo(path) - // repo.getLineDiffs(repo.relativize(path), text, options) - throw new Error('Unimplemented') + return this.repoPromise + .then(repo => repo.getHeadCommit()) + .then(commit => commit.getEntry(_path)) + .then(entry => entry.getBlob()) + .then(blob => { + const options = new Git.DiffOptions() + if (process.platform === 'win32') { + // Ignore eol of line differences on windows so that files checked in + // as LF don't report every line modified when the text contains CRLF + // endings. + // TODO: set GIT_DIFF_IGNORE_WHITESPACE_EOL + } + return this._diffBlobToBuffer(blob, text, options) + }) + .then(hunks => { + // TODO: The old implementation just takes into account the last hunk. + // That's probably safe for most cases but maybe wrong in some edge + // cases? + const hunk = hunks[hunks.length - 1] + return { + oldStart: hunk.oldStart(), + newStart: hunk.newStart(), + oldLines: hunk.oldLines(), + newLines: hunk.newLines() + } + }) } // Checking Out From cea6926cf26647d5bad7dccadffc0769d2692e82 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 15:46:36 -0500 Subject: [PATCH 134/254] Set IGNORE_WHITESPACE_EOL on Windows. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f0c394a02..116f52216 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -511,7 +511,7 @@ export default class GitRepositoryAsync { // Ignore eol of line differences on windows so that files checked in // as LF don't report every line modified when the text contains CRLF // endings. - // TODO: set GIT_DIFF_IGNORE_WHITESPACE_EOL + options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL } return this._diffBlobToBuffer(blob, text, options) }) From 0e3bb42b2cb830ab628453350391554c3f07a4e3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 4 Dec 2015 12:01:51 -0500 Subject: [PATCH 135/254] Use some binaries, hopefully? --- .npmrc | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index c5ff09782..d71d2a4c4 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +runtime = electron +target = 0.34.5 +disturl = https://atom.io/download/atom-shell diff --git a/package.json b/package.json index 77661a010..87873082a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit": "~0.5.0", + "nodegit": "0.0.10000002", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 7691107e4ba468b14dc3a25d2d77405e752a9ea4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 4 Dec 2015 12:27:52 -0500 Subject: [PATCH 136/254] Added license override for inherit. --- build/tasks/license-overrides.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index 19f32fb84..aa136eb37 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -328,3 +328,16 @@ module.exports = modification. This license may not be modified without the express written permission of its copyright owner. """ + 'inherit@2.2.2': + license: 'MIT' + repository: 'https://github.com/dfilatov/inherit' + source: 'LICENSE.md' + sourceText: """ + Copyright (c) 2012 Dmitry Filatov + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ From f5f2cd372d4c872b39db3a792809292d44822b16 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:09:04 -0500 Subject: [PATCH 137/254] Use nodegit-atom for now. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87873082a..2ccf080dd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit": "0.0.10000002", + "nodegit-atom": "0.0.1", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 09bd603c4b0bafd3113061fee041525af09d22c5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:10:20 -0500 Subject: [PATCH 138/254] Don't set these here. --- .npmrc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.npmrc b/.npmrc index d71d2a4c4..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -runtime = electron -target = 0.34.5 -disturl = https://atom.io/download/atom-shell From 8514cb18cdc34769552b13ac12b57a18c757e7ea Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:10:28 -0500 Subject: [PATCH 139/254] Do the needful to get Electron binaries. --- script/cibuild | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/script/cibuild b/script/cibuild index 860e0a938..5f76a6eee 100755 --- a/script/cibuild +++ b/script/cibuild @@ -45,6 +45,13 @@ function setEnvironmentVariables() { process.env.BUILD_ATOM_RELEASES_S3_SECRET = process.env.BUILD_ATOM_WIN_RELEASES_S3_SECRET process.env.BUILD_ATOM_RELEASES_S3_BUCKET = process.env.BUILD_ATOM_WIN_RELEASES_S3_BUCKET } + + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + process.env.npm_config_runtime = 'electron'; + // TODO: get this from package.json + process.env.npm_config_target = '0.34.5'; + process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; } function removeNodeModules() { From 752b266712b51667bb1f1c26829f32f575f5c154 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:19:47 -0500 Subject: [PATCH 140/254] Use the same version as The Real Nodegit. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ccf080dd..6b15afb0d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit-atom": "0.0.1", + "nodegit-atom": "0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From ed357b1bb67f6b50d95f46dc2ffbe2b51c5d757e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:52:28 -0500 Subject: [PATCH 141/254] ++nodegit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b15afb0d..6aa81afbd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit-atom": "0.5.0", + "nodegit-atom": "0.5.1", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 7da15beb71eb21d0dfe7e3ee0ea0a6f44f3a1180 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 15:39:23 -0500 Subject: [PATCH 142/254] ++nodegit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6aa81afbd..d766d8dd9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit-atom": "0.5.1", + "nodegit": "0.6.0", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 0baf653b99094f29c287a9bec72fd8ebfc5cf37a Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 15:44:38 -0500 Subject: [PATCH 143/254] Provide these from .npmrc. --- .npmrc | 3 +++ script/cibuild | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.npmrc b/.npmrc index c5ff09782..d71d2a4c4 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +runtime = electron +target = 0.34.5 +disturl = https://atom.io/download/atom-shell diff --git a/script/cibuild b/script/cibuild index 5f76a6eee..860e0a938 100755 --- a/script/cibuild +++ b/script/cibuild @@ -45,13 +45,6 @@ function setEnvironmentVariables() { process.env.BUILD_ATOM_RELEASES_S3_SECRET = process.env.BUILD_ATOM_WIN_RELEASES_S3_SECRET process.env.BUILD_ATOM_RELEASES_S3_BUCKET = process.env.BUILD_ATOM_WIN_RELEASES_S3_BUCKET } - - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - process.env.npm_config_runtime = 'electron'; - // TODO: get this from package.json - process.env.npm_config_target = '0.34.5'; - process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; } function removeNodeModules() { From b810a8b491c22f7e09db6d960fab31a90e0d8876 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 17:04:41 -0500 Subject: [PATCH 144/254] Get the Electron version from the package. --- .npmrc | 3 --- script/bootstrap | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.npmrc b/.npmrc index d71d2a4c4..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -runtime = electron -target = 0.34.5 -disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 8314b9cb0..d62b84375 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -64,6 +64,12 @@ function bootstrap() { process.env.ATOM_RESOURCE_PATH = path.resolve(__dirname, '..'); + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + process.env.npm_config_target = require('../package.json').electronVersion; + process.env.npm_config_runtime = 'electron'; + process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; + var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; From ab94103497803673b59fd28a4b296000cf5ed148 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 23:19:09 -0500 Subject: [PATCH 145/254] Back to putting them in the npmrc. --- .npmrc | 3 +++ script/bootstrap | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.npmrc b/.npmrc index c5ff09782..6fc4bd7ae 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +target = 0.34.5 +runtime = electron +disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index d62b84375..8314b9cb0 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -64,12 +64,6 @@ function bootstrap() { process.env.ATOM_RESOURCE_PATH = path.resolve(__dirname, '..'); - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - process.env.npm_config_target = require('../package.json').electronVersion; - process.env.npm_config_runtime = 'electron'; - process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; - var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; From cdf1029698bd5d76822acf0b3ba64ea1264dee98 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 23:54:12 -0500 Subject: [PATCH 146/254] It's fine if there isn't an upstream. --- src/git-repository-async.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 116f52216..42e53f105 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -210,7 +210,10 @@ export default class GitRepositoryAsync { getAheadBehindCount (reference, _path) { return this._getRepo(_path) .then(repo => Promise.all([repo, repo.getBranch(reference)])) - .then(([repo, local]) => Promise.all([repo, local, Git.Branch.upstream(local)])) + .then(([repo, local]) => { + const upstream = Git.Branch.upstream(local).catch(_ => null) + return Promise.all([repo, local, upstream]) + }) .then(([repo, local, upstream]) => { if (!upstream) return {ahead: 0, behind: 0} From f0a9c5d9f09c3dd4e263322d0c4e567afb818518 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:33:56 -0500 Subject: [PATCH 147/254] Just use the repo declared above. --- spec/git-repository-async-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 9aca3356c..0bc390814 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -55,8 +55,6 @@ describe('GitRepositoryAsync', () => { }) describe('.isPathIgnored(path)', () => { - let repo - beforeEach(() => { repo = openFixture('ignore.git') }) From 46d59fdda10f65b5647e935c571e3696ecbe5578 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:34:49 -0500 Subject: [PATCH 148/254] Fix usage when we're given no options. --- src/git-repository-async.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 42e53f105..c1d42bc41 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -34,7 +34,9 @@ export default class GitRepositoryAsync { this._refreshingCount = 0 - let {refreshOnWindowFocus} = options || true + options = options || {} + + let {refreshOnWindowFocus = true} = options if (refreshOnWindowFocus) { const onWindowFocus = () => this.refreshStatus() window.addEventListener('focus', onWindowFocus) From 4711d0367532ecb360548fea128556fa3833c52c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:35:07 -0500 Subject: [PATCH 149/254] Tests for .checkoutHeadForEditor. --- spec/git-repository-async-spec.js | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0bc390814..d344fbb2f 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -203,6 +203,41 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.checkoutHeadForEditor(editor)', () => { + let filePath + let editor + + beforeEach(() => { + spyOn(atom, "confirm") + + const workingDirPath = copyRepository() + repo = new GitRepositoryAsync(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise(() => atom.workspace.open(filePath)) + runs(() => editor = atom.workspace.getActiveTextEditor()) + }) + + it('displays a confirmation dialog by default', async () => { + atom.confirm.andCallFake(({buttons}) => buttons.OK()) + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + await repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('does not display a dialog when confirmation is disabled', async () => { + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + await repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath From 7f19cd4f17b100e9c8fb1be4227f0f6062dbab16 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:35:46 -0500 Subject: [PATCH 150/254] Throw if we try to use the repository after it's been destroyed. --- src/git-repository-async.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index c1d42bc41..8b3acef3c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -61,6 +61,8 @@ export default class GitRepositoryAsync { this.subscriptions.dispose() this.subscriptions = null } + + this.repoPromise = null } // Event subscription @@ -213,14 +215,13 @@ export default class GitRepositoryAsync { return this._getRepo(_path) .then(repo => Promise.all([repo, repo.getBranch(reference)])) .then(([repo, local]) => { - const upstream = Git.Branch.upstream(local).catch(_ => null) + const upstream = Git.Branch.upstream(local) return Promise.all([repo, local, upstream]) }) .then(([repo, local, upstream]) => { - if (!upstream) return {ahead: 0, behind: 0} - return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) }) + .catch(_ => ({ahead: 0, behind: 0})) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -664,7 +665,15 @@ export default class GitRepositoryAsync { return this._refreshingCount === 0 } + _destroyed() { + return this.repoPromise == null + } + _getRepo (_path) { + if (this._destroyed()) { + return Promise.reject(new Error('Repository has been destroyed')) + } + if (!_path) return this.repoPromise return this.isSubmodule(_path) From 86e39a22ffe17fdd49391a14599b5308db0dde8e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:35:53 -0500 Subject: [PATCH 151/254] Test .destroy(). --- spec/git-repository-async-spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d344fbb2f..1f8768593 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -238,6 +238,21 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.destroy()', () => { + it('throws an exception when any method is called after it is called', async () => { + repo = new GitRepositoryAsync(require.resolve('./fixtures/git/master.git/HEAD')) + repo.destroy() + + let threw = false + try { + await repo.getShortHead() + } catch (e) { + threw = true + } + expect(threw).toBeTruthy() + }) + }) + describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath From e867b68eab9b691f91cecb401f320cf811458c1d Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:38:15 -0500 Subject: [PATCH 152/254] Match the ordering of the GitRepository spec. --- spec/git-repository-async-spec.js | 52 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 1f8768593..9c49980f2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -177,32 +177,6 @@ describe('GitRepositoryAsync', () => { }) }) - describe('.getPathStatus(path)', () => { - let filePath - - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - }) - - it('trigger a status-changed event when the new status differs from the last cached one', async () => { - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - fs.writeFileSync(filePath, '') - - await repo.getPathStatus(filePath) - - expect(statusHandler.callCount).toBe(1) - const status = Git.Status.STATUS.WT_MODIFIED - expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) - fs.writeFileSync(filePath, 'abc') - - await repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe(1) - }) - }) - describe('.checkoutHeadForEditor(editor)', () => { let filePath let editor @@ -253,6 +227,32 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.getPathStatus(path)', () => { + let filePath + + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + }) + + it('trigger a status-changed event when the new status differs from the last cached one', async () => { + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + fs.writeFileSync(filePath, '') + + await repo.getPathStatus(filePath) + + expect(statusHandler.callCount).toBe(1) + const status = Git.Status.STATUS.WT_MODIFIED + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) + fs.writeFileSync(filePath, 'abc') + + await repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath From 352f4064e9720b91b77ad477321492074899dc5b Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 11:05:55 -0500 Subject: [PATCH 153/254] Add a whole ton of missing documentation. --- src/git-repository-async.js | 237 ++++++++++++++++++++++++++++-------- 1 file changed, 187 insertions(+), 50 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8b3acef3c..cb2c8ffae 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -51,6 +51,10 @@ export default class GitRepositoryAsync { } } + // Public: Destroy this {GitRepositoryAsync} object. + // + // This destroys any tasks and subscriptions and releases the underlying + // libgit2 repository handle. This method is idempotent. destroy () { if (this.emitter) { this.emitter.emit('did-destroy') @@ -68,14 +72,39 @@ export default class GitRepositoryAsync { // Event subscription // ================== + // Public: Invoke the given callback when this GitRepositoryAsync's destroy() + // method is invoked. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy (callback) { return this.emitter.on('did-destroy', callback) } + // Public: Invoke the given callback when a specific file's status has + // changed. When a file is updated, reloaded, etc, and the status changes, this + // will be fired. + // + // * `callback` {Function} + // * `event` {Object} + // * `path` {String} the old parameters the decoration used to have + // * `pathStatus` {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatus (callback) { return this.emitter.on('did-change-status', callback) } + // Public: Invoke the given callback when a multiple files' statuses have + // changed. For example, on window focus, the status of all the paths in the + // repo is checked. If any of them have changed, this will be fired. Call + // {::getPathStatus(path)} to get the status for your path of choice. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses (callback) { return this.emitter.on('did-change-statuses', callback) } @@ -178,7 +207,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to a {String}. getShortHead (_path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.getCurrentBranch()) .then(branch => branch.shorthand()) } @@ -212,7 +241,7 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getAheadBehindCount (reference, _path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => Promise.all([repo, repo.getBranch(reference)])) .then(([repo, local]) => { const upstream = Git.Branch.upstream(local) @@ -245,7 +274,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} git configuration value // specified by the key. getConfigValue (key, _path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.configSnapshot()) .then(config => config.getStringBuf(key)) .catch(_ => null) @@ -271,7 +300,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {String} branch name such as // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.getCurrentBranch()) .then(branch => Git.Branch.upstream(branch)) } @@ -286,7 +315,7 @@ export default class GitRepositoryAsync { // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) .then(refs => { const heads = [] @@ -314,7 +343,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the current {String} SHA for the // given reference. getReferenceTarget (reference, _path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => Git.Reference.nameToId(repo, reference)) .then(oid => oid.tostrS()) } @@ -322,18 +351,36 @@ export default class GitRepositoryAsync { // Reading Status // ============== + // Public: Resolves true if the given path is modified. + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` + // is modified. isPathModified (_path) { return this._filterStatusesByPath(_path).then(statuses => { return statuses.filter(status => status.isModified()).length > 0 }) } + // Public: Resolves true if the given path is new. + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` + // is new. isPathNew (_path) { return this._filterStatusesByPath(_path).then(statuses => { return statuses.filter(status => status.isNew()).length > 0 }) } + // Public: Is the given path ignored? + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` + // is ignored. isPathIgnored (_path) { return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) } @@ -344,7 +391,6 @@ export default class GitRepositoryAsync { // // Returns a promise resolving to a {Number} representing the status. This value can be passed to // {::isStatusModified} or {::isStatusNew} to get more information. - getDirectoryStatus (directoryPath) { let relativePath // XXX _filterSBD already gets repoPromise @@ -419,38 +465,54 @@ export default class GitRepositoryAsync { .then(relativePath => this.pathStatusCache[relativePath]) } - isStatusNew (statusBit) { - return (statusBit & newStatusFlags) > 0 - } - + // Public: Returns true if the given status indicates modification. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates modification. isStatusModified (statusBit) { return (statusBit & modifiedStatusFlags) > 0 } + // Public: Returns true if the given status indicates a new path. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates a new path. + isStatusNew (statusBit) { + return (statusBit & newStatusFlags) > 0 + } + + // Public: Returns true if the given status indicates the path is staged. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates the path is + // staged. isStatusStaged (statusBit) { return (statusBit & indexStatusFlags) > 0 } + // Public: Returns true if the given status indicates the path is ignored. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates the path is + // ignored. isStatusIgnored (statusBit) { return (statusBit & (1 << 14)) > 0 } + // Public: Returns true if the given status indicates the path is deleted. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates the path is + // deleted. isStatusDeleted (statusBit) { return (statusBit & deletedStatusFlags) > 0 } - _getDiffHunks (diff) { - return diff.patches() - .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array - .then(hunks => _.flatten(hunks)) // hunks :: Array> - } - - _getDiffLines (diff) { - return this._getDiffHunks(diff) - .then(hunks => Promise.all(hunks.map(h => h.lines()))) - .then(lines => _.flatten(lines)) // lines :: Array> - } - // Retrieving Diffs // ================ // Public: Retrieves the number of lines added and removed to a path. @@ -487,14 +549,6 @@ export default class GitRepositoryAsync { }) } - _diffBlobToBuffer (blob, buffer, options) { - const hunks = [] - const hunkCallback = (delta, hunk, payload) => { - hunks.push(hunk) - } - return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null).then(_ => hunks) - } - // Public: Retrieves the line diffs comparing the `HEAD` version of the given // path and the given text. // @@ -563,12 +617,6 @@ export default class GitRepositoryAsync { .then(() => this.refreshStatusForPath(_path)) } - _createBranch (name) { - return this.repoPromise - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, commit]) => repo.createBranch(name, commit)) - } - // Public: Checks out a branch in your repository. // // * `reference` The {String} reference to checkout. @@ -607,6 +655,57 @@ export default class GitRepositoryAsync { }).then(filePath => this.checkoutHead(filePath)) } + // Create a new branch with the given name. + // + // name :: String + // The name of the new branch. + // + // Returns :: Promise + // A reference to the created branch. + _createBranch (name) { + return this.repoPromise + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, commit]) => repo.createBranch(name, commit)) + } + + // Get all the hunks in the diff. + // + // diff :: NodeGit.Diff + // + // Returns :: Promise> + _getDiffHunks (diff) { + return diff.patches() + .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array + .then(hunks => _.flatten(hunks)) // hunks :: Array> + } + + // Get all the lines contained in the diff. + // + // diff :: NodeGit.Diff + // + // Returns :: Promise> + _getDiffLines (diff) { + return this._getDiffHunks(diff) + .then(hunks => Promise.all(hunks.map(h => h.lines()))) + .then(lines => _.flatten(lines)) // lines :: Array> + } + + // Diff the given blob and buffer with the provided options. + // + // blob :: NodeGit.Blob + // buffer :: String + // options :: NodeGit.DiffOptions + // + // Returns :: Promise> + _diffBlobToBuffer (blob, buffer, options) { + const hunks = [] + const hunkCallback = (delta, hunk, payload) => { + hunks.push(hunk) + } + return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null) + .then(_ => hunks) + } + // Get the current branch and update this.branch. // // Returns :: Promise @@ -618,11 +717,21 @@ export default class GitRepositoryAsync { .then(branchName => this.branch = branchName) } + // Refresh the cached ahead/behind count with the given branch. + // + // branchName :: String + // The name of the branch whose ahead/behind should be used for + // the refresh. + // + // Returns :: Promise _refreshAheadBehindCount (branchName) { return this.getAheadBehindCount(branchName) .then(counts => this.upstreamByPath['.'] = counts) } + // Refresh the cached status. + // + // Returns :: Promise _refreshStatus () { this._refreshingCount++ @@ -658,18 +767,14 @@ export default class GitRepositoryAsync { return Promise.all([status, branch, aheadBehind]).then(_ => null) } - // Section: Private - // ================ - - _isRefreshing () { - return this._refreshingCount === 0 - } - - _destroyed() { - return this.repoPromise == null - } - - _getRepo (_path) { + // Get the NodeGit repository for the given path. + // + // path :: Optional + // The path within the repository. This is only needed if you want + // to get the repository for that path if it is a submodule. + // + // Returns :: Promise + getRepo (_path) { if (this._destroyed()) { return Promise.reject(new Error('Repository has been destroyed')) } @@ -686,6 +791,24 @@ export default class GitRepositoryAsync { }) } + // Section: Private + // ================ + + // Is the repository currently refreshing its status? + // + // Returns :: Bool + _isRefreshing () { + return this._refreshingCount === 0 + } + + // Has the repository been destroyed? + // + // Returns :: Bool + _destroyed() { + return this.repoPromise == null + } + + // Subscribe to events on the given buffer. subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() @@ -710,8 +833,15 @@ export default class GitRepositoryAsync { return } + // Get the status for the given path. + // + // path :: String + // The path whose status is wanted. + // + // Returns :: Promise + // The status for the path. _filterStatusesByPath (_path) { - // Surely I'm missing a built-in way to do this + // TODO: Is there a more efficient way to do this? let basePath = null return this.repoPromise .then(repo => { @@ -723,6 +853,13 @@ export default class GitRepositoryAsync { }) } + // Get the status for everything in the given directory. + // + // directoryPath :: String + // The directory whose status is wanted. + // + // Returns :: Promise> + // The status for every file in the directory. _filterStatusesByDirectory (directoryPath) { return this.repoPromise .then(repo => repo.getStatus()) From abf5697240900426e37c60cba45bc6d1fe552ae7 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 11:33:01 -0500 Subject: [PATCH 154/254] Delint --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index cb2c8ffae..dcb677bd4 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -804,7 +804,7 @@ export default class GitRepositoryAsync { // Has the repository been destroyed? // // Returns :: Bool - _destroyed() { + _destroyed () { return this.repoPromise == null } From 47606507f7712c3cae1cb978ff3d7924b5096297 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:31:36 -0500 Subject: [PATCH 155/254] We want the booleans. --- spec/git-repository-async-spec.js | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 9c49980f2..2db1fa7da 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -41,7 +41,7 @@ describe('GitRepositoryAsync', () => { threw = true } - expect(threw).toBeTruthy() + expect(threw).toBe(true) expect(repo.repo).toBe(null) }) }) @@ -61,12 +61,12 @@ describe('GitRepositoryAsync', () => { it('resolves true for an ignored path', async () => { const ignored = await repo.isPathIgnored('a.txt') - expect(ignored).toBeTruthy() + expect(ignored).toBe(true) }) it('resolves false for a non-ignored path', async () => { const ignored = await repo.isPathIgnored('b.txt') - expect(ignored).toBeFalsy() + expect(ignored).toBe(false) }) }) @@ -85,23 +85,23 @@ describe('GitRepositoryAsync', () => { describe('when the path is unstaged', () => { it('resolves false if the path has not been modified', async () => { const modified = await repo.isPathModified(filePath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, 'change') const modified = await repo.isPathModified(filePath) - expect(modified).toBeTruthy() + expect(modified).toBe(true) }) it('resolves false if the path is new', async () => { const modified = await repo.isPathModified(newPath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) it('resolves false if the path is invalid', async () => { const modified = await repo.isPathModified(emptyPath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) }) }) @@ -119,12 +119,12 @@ describe('GitRepositoryAsync', () => { describe('when the path is unstaged', () => { it('returns true if the path is new', async () => { const isNew = await repo.isPathNew(newPath) - expect(isNew).toBeTruthy() + expect(isNew).toBe(true) }) it("returns false if the path isn't new", async () => { const modified = await repo.isPathModified(newPath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) }) }) @@ -140,17 +140,17 @@ describe('GitRepositoryAsync', () => { it('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) fs.writeFileSync(filePath, 'ch ch changes') modified = await repo.isPathModified(filePath) - expect(modified).toBeTruthy() + expect(modified).toBe(true) await repo.checkoutHead(filePath) modified = await repo.isPathModified(filePath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) it('restores the contents of the path to the original text', async () => { @@ -223,7 +223,7 @@ describe('GitRepositoryAsync', () => { } catch (e) { threw = true } - expect(threw).toBeTruthy() + expect(threw).toBe(true) }) }) @@ -295,8 +295,8 @@ describe('GitRepositoryAsync', () => { await repo.refreshStatus() expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBe(true) + expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true) }) }) @@ -510,12 +510,12 @@ describe('GitRepositoryAsync', () => { it('resolves true when the branch exists', async () => { const hasBranch = await repo.hasBranch('master') - expect(hasBranch).toBeTruthy() + expect(hasBranch).toBe(true) }) it("resolves false when the branch doesn't exist", async () => { const hasBranch = await repo.hasBranch('trolleybus') - expect(hasBranch).toBeFalsy() + expect(hasBranch).toBe(false) }) }) @@ -575,8 +575,8 @@ describe('GitRepositoryAsync', () => { .then(_ => success = true) .catch(_ => threw = true)) runs(() => { - expect(success).toBeTruthy() - expect(threw).toBeFalsy() + expect(success).toBe(true) + expect(threw).toBe(false) }) }) }) From be964abf9ff96f62bbb0d0543e2f71cc5d4a62e5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:31:46 -0500 Subject: [PATCH 156/254] Ensure we actually do give them a boolean. --- src/git-repository-async.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index dcb677bd4..dea3baec0 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -382,7 +382,9 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + return this.repoPromise + .then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + .then(ignored => Boolean(ignored)) } // Get the status of a directory in the repository's working directory. From 1a04c5a9c23c3c09b9bff389e1f17b3099130ed5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:41:38 -0500 Subject: [PATCH 157/254] Be more specific in our error names. --- spec/git-repository-async-spec.js | 6 +++--- src/git-repository-async.js | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2db1fa7da..83d746ad2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -217,13 +217,13 @@ describe('GitRepositoryAsync', () => { repo = new GitRepositoryAsync(require.resolve('./fixtures/git/master.git/HEAD')) repo.destroy() - let threw = false + let error = null try { await repo.getShortHead() } catch (e) { - threw = true + error = e } - expect(threw).toBe(true) + expect(error.name).toBe(GitRepositoryAsync.DestroyedErrorName) }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index dea3baec0..5db475194 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -23,6 +23,10 @@ export default class GitRepositoryAsync { return Git } + static get DestroyedErrorName () { + return 'GitRepositoryAsync.destroyed' + } + constructor (_path, options) { this.repo = null this.emitter = new Emitter() @@ -778,7 +782,9 @@ export default class GitRepositoryAsync { // Returns :: Promise getRepo (_path) { if (this._destroyed()) { - return Promise.reject(new Error('Repository has been destroyed')) + const error = new Error('Repository has been destroyed') + error.name = GitRepositoryAsync.DestroyedErrorName + return Promise.reject(error) } if (!_path) return this.repoPromise From bc14c28f11617b96cb9bac6cd6d370a73f6a3824 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:42:11 -0500 Subject: [PATCH 158/254] Use the usual name. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 83d746ad2..82d4b6f24 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -301,7 +301,7 @@ describe('GitRepositoryAsync', () => { }) describe('buffer events', () => { - let repository + let repo beforeEach(() => { const workingDirectory = copyRepository() From 0794ea365d874fc8e97ef9c79641a48d6af4e0ab Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:43:25 -0500 Subject: [PATCH 159/254] Tinker tailor soldier spy. --- spec/git-repository-async-spec.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 82d4b6f24..74bf8ef16 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -319,12 +319,15 @@ describe('GitRepositoryAsync', () => { editor.insertNewline() - let called - repository.onDidChangeStatus(c => called = c) + const statusHandler = jasmine.createSpy('statusHandler') + repository.onDidChangeStatus(statusHandler) editor.save() - waitsFor(() => Boolean(called)) - runs(() => expect(called).toEqual({path: editor.getPath(), pathStatus: 256})) + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBeGreaterThan(0) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + }) }) it('emits a status-changed event when a buffer is reloaded', async () => { From 5302fb84a944e3db98a484966f89934e2b7ff312 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:44:51 -0500 Subject: [PATCH 160/254] Better test name. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 74bf8ef16..8850bd9a7 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -590,7 +590,7 @@ describe('GitRepositoryAsync', () => { repo = GitRepositoryAsync.open(workingDirectory) }) - it('can get the line diff', async () => { + it('returns the old and new lines of the diff', async () => { const {oldStart, newStart, oldLines, newLines} = await repo.getLineDiffs('a.txt', 'hi there') expect(oldStart).toBe(0) expect(oldLines).toBe(0) From fcf8581255b50afe9245da468ed1a38d6984c084 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:45:05 -0500 Subject: [PATCH 161/254] Put this constant with the others. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 5db475194..4b97323d2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,6 +9,7 @@ const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.IN const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE +const submoduleMode = 57344 // TODO compose this from libgit2 constants // Just using this for _.isEqual and _.object, we should impl our own here import _ from 'underscore-plus' @@ -229,7 +230,6 @@ export default class GitRepositoryAsync { // TODO: This'll probably be wrong if the submodule doesn't exist in the // index yet? Is that a thing? const entry = index.getByPath(_path) - const submoduleMode = 57344 // TODO compose this from libgit2 constants return entry.mode === submoduleMode }) } From 3e7344394c01963a501b88732131ec93ebcff662 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:54:15 -0500 Subject: [PATCH 162/254] Fix the documentation style. --- src/git-repository-async.js | 76 +++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4b97323d2..d96cb1b7c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -395,8 +395,9 @@ export default class GitRepositoryAsync { // // * `directoryPath` The {String} path to check. // - // Returns a promise resolving to a {Number} representing the status. This value can be passed to - // {::isStatusModified} or {::isStatusNew} to get more information. + // Returns a {Promise} resolving to a {Number} representing the status. This + // value can be passed to {::isStatusModified} or {::isStatusNew} to get more + // information. getDirectoryStatus (directoryPath) { let relativePath // XXX _filterSBD already gets repoPromise @@ -423,11 +424,10 @@ export default class GitRepositoryAsync { // Note that if the status of the path has changed, this will emit a // 'did-change-status' event. // - // path :: String - // The path whose status should be refreshed. + // * `path` The {String} path whose status should be refreshed. // - // Returns :: Promise - // The refreshed status bit for the path. + // Returns a {Promise} which resolves to a {Number} which is the refreshed + // status bit for the path. refreshStatusForPath (_path) { this._refreshingCount++ @@ -663,11 +663,10 @@ export default class GitRepositoryAsync { // Create a new branch with the given name. // - // name :: String - // The name of the new branch. + // * `name` The {String} name of the new branch. // - // Returns :: Promise - // A reference to the created branch. + // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the + // created branch. _createBranch (name) { return this.repoPromise .then(repo => Promise.all([repo, repo.getHeadCommit()])) @@ -676,9 +675,9 @@ export default class GitRepositoryAsync { // Get all the hunks in the diff. // - // diff :: NodeGit.Diff + // * `diff` The {NodeGit.Diff} whose hunks should be retrieved. // - // Returns :: Promise> + // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. _getDiffHunks (diff) { return diff.patches() .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array @@ -687,9 +686,9 @@ export default class GitRepositoryAsync { // Get all the lines contained in the diff. // - // diff :: NodeGit.Diff + // * `diff` The {NodeGit.Diff} use lines should be retrieved. // - // Returns :: Promise> + // Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}. _getDiffLines (diff) { return this._getDiffHunks(diff) .then(hunks => Promise.all(hunks.map(h => h.lines()))) @@ -698,11 +697,11 @@ export default class GitRepositoryAsync { // Diff the given blob and buffer with the provided options. // - // blob :: NodeGit.Blob - // buffer :: String - // options :: NodeGit.DiffOptions + // * `blob` The {NodeGit.Blob} + // * `buffer` The {String} buffer. + // * `options` The {NodeGit.DiffOptions} // - // Returns :: Promise> + // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. _diffBlobToBuffer (blob, buffer, options) { const hunks = [] const hunkCallback = (delta, hunk, payload) => { @@ -714,8 +713,7 @@ export default class GitRepositoryAsync { // Get the current branch and update this.branch. // - // Returns :: Promise - // The branch name. + // Returns a {Promise} which resolves to the {String} branch name. _refreshBranch () { return this.repoPromise .then(repo => repo.getCurrentBranch()) @@ -725,11 +723,10 @@ export default class GitRepositoryAsync { // Refresh the cached ahead/behind count with the given branch. // - // branchName :: String - // The name of the branch whose ahead/behind should be used for - // the refresh. + // * `branchName` The {String} name of the branch whose ahead/behind should be + // used for the refresh. // - // Returns :: Promise + // Returns a {Promise} which will resolve to {null}. _refreshAheadBehindCount (branchName) { return this.getAheadBehindCount(branchName) .then(counts => this.upstreamByPath['.'] = counts) @@ -737,7 +734,7 @@ export default class GitRepositoryAsync { // Refresh the cached status. // - // Returns :: Promise + // Returns a {Promise} which will resolve to {null}. _refreshStatus () { this._refreshingCount++ @@ -761,8 +758,7 @@ export default class GitRepositoryAsync { // Refreshes the git status. // - // Returns :: Promise - // Resolves when refresh has completed. + // Returns a {Promise} which will resolve to {null} when refresh is complete. refreshStatus () { // TODO add submodule tracking @@ -775,11 +771,11 @@ export default class GitRepositoryAsync { // Get the NodeGit repository for the given path. // - // path :: Optional - // The path within the repository. This is only needed if you want - // to get the repository for that path if it is a submodule. + // * `path` The optional {String} path within the repository. This is only + // needed if you want to get the repository for that path if it is a + // submodule. // - // Returns :: Promise + // Returns a {Promise} which resolves to the {NodeGit.Repository}. getRepo (_path) { if (this._destroyed()) { const error = new Error('Repository has been destroyed') @@ -804,14 +800,14 @@ export default class GitRepositoryAsync { // Is the repository currently refreshing its status? // - // Returns :: Bool + // Returns a {Boolean}. _isRefreshing () { return this._refreshingCount === 0 } // Has the repository been destroyed? // - // Returns :: Bool + // Returns a {Boolean}. _destroyed () { return this.repoPromise == null } @@ -843,11 +839,10 @@ export default class GitRepositoryAsync { // Get the status for the given path. // - // path :: String - // The path whose status is wanted. + // * `path` The {String} path whose status is wanted. // - // Returns :: Promise - // The status for the path. + // Returns a {Promise} which resolves to the {NodeGit.StatusFile} status for + // the path. _filterStatusesByPath (_path) { // TODO: Is there a more efficient way to do this? let basePath = null @@ -863,11 +858,10 @@ export default class GitRepositoryAsync { // Get the status for everything in the given directory. // - // directoryPath :: String - // The directory whose status is wanted. + // * `directoryPath` The {String} directory whose status is wanted. // - // Returns :: Promise> - // The status for every file in the directory. + // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} + // statuses for every file in the directory. _filterStatusesByDirectory (directoryPath) { return this.repoPromise .then(repo => repo.getStatus()) From 4916a2e7626f5089b8da2fbe2da89ba992c4d92e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:55:08 -0500 Subject: [PATCH 163/254] :doughnut: need this. --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d96cb1b7c..afc5795c9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -834,7 +834,6 @@ export default class GitRepositoryAsync { ) this.subscriptions.add(bufferSubscriptions) - return } // Get the status for the given path. From 0c42747e8a62f8ce0c0bcd8b9f5335e60e87a07e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:55:21 -0500 Subject: [PATCH 164/254] Just Use Clone. --- src/git-repository.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 5a0df6743..6ab6be6a5 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -76,9 +76,7 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - asyncOptions = {} - for key, val of options - asyncOptions[key] = val + asyncOptions = _.clone(options) # GitRepository itself will handle these cases by manually calling through # to the async repo. asyncOptions.refreshOnWindowFocus = false From 3ccf1861506e1cd37797134502ff1d0a4758ba1b Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 17:57:13 -0500 Subject: [PATCH 165/254] This can be local. --- src/git-repository-async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index afc5795c9..8d58a0066 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -399,11 +399,10 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - let relativePath // XXX _filterSBD already gets repoPromise return this.repoPromise .then(repo => { - relativePath = this.relativize(directoryPath, repo.workdir()) + const relativePath = this.relativize(directoryPath, repo.workdir()) return this._filterStatusesByDirectory(relativePath) }) .then(statuses => { From dba698f672df8c1e2e1b69e1370d8b2212b53186 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 17:57:21 -0500 Subject: [PATCH 166/254] Define a constant for this. --- src/git-repository-async.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8d58a0066..296c29adc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,7 +9,8 @@ const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.IN const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -const submoduleMode = 57344 // TODO compose this from libgit2 constants +const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants +const submoduleMode = 57344 // TODO: compose this from libgit2 constants // Just using this for _.isEqual and _.object, we should impl our own here import _ from 'underscore-plus' @@ -505,7 +506,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // ignored. isStatusIgnored (statusBit) { - return (statusBit & (1 << 14)) > 0 + return (statusBit & ignoredStatusFlags) > 0 } // Public: Returns true if the given status indicates the path is deleted. From c5383305d081d504d8e75da9c5dd0e1c00bf1d0c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 17:57:35 -0500 Subject: [PATCH 167/254] If we don't have a project then we're not at the root. --- src/git-repository-async.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 296c29adc..a29b8f033 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -141,13 +141,15 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} that resolves to true if at the root, false if // in a subfolder of the repository. isProjectAtRoot () { - if (!this.projectAtRoot && this.project) { + if (!this.project) return Promise.resolve(false) + + if (this.projectAtRoot) { + return this.projectAtRoot + } else { this.projectAtRoot = Promise.resolve(() => { return this.repoPromise.then(repo => this.project.relativize(repo.workdir())) }) } - - return this.projectAtRoot } // Public: Makes a path relative to the repository's working directory. From 81d4f0680280fd20dc105c1daa575cb2017b84c7 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 20:00:37 -0500 Subject: [PATCH 168/254] And that's why we write tests. --- spec/git-repository-async-spec.js | 29 ++++++++++++++++++++++++----- src/git-repository-async.js | 12 ++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 8850bd9a7..a9f1f9997 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -300,6 +300,25 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.isProjectAtRoot()', () => { + it('returns true when the repository is at the root', async () => { + const workingDirectory = copyRepository() + atom.project.setPaths([workingDirectory]) + const repo = atom.project.getRepositories()[0].async + + const atRoot = await repo.isProjectAtRoot() + expect(atRoot).toBe(true) + }) + + it("returns false when the repository wasn't created with a project", async () => { + const workingDirectory = copyRepository() + const repo = GitRepositoryAsync.open(workingDirectory) + + const atRoot = await repo.isProjectAtRoot() + expect(atRoot).toBe(false) + }) + }) + describe('buffer events', () => { let repo @@ -310,8 +329,8 @@ describe('GitRepositoryAsync', () => { // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that // we're in a known state. - repository = atom.project.getRepositories()[0].async - waitsFor(() => !repository._isRefreshing()) + repo = atom.project.getRepositories()[0].async + waitsFor(() => !repo._isRefreshing()) }) it('emits a status-changed event when a buffer is saved', async () => { @@ -320,7 +339,7 @@ describe('GitRepositoryAsync', () => { editor.insertNewline() const statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) editor.save() waitsFor(() => statusHandler.callCount > 0) @@ -336,7 +355,7 @@ describe('GitRepositoryAsync', () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) editor.getBuffer().reload() waitsFor(() => statusHandler.callCount > 0) @@ -352,7 +371,7 @@ describe('GitRepositoryAsync', () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') waitsFor(() => statusHandler.callCount > 0) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a29b8f033..a87c93973 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -143,13 +143,13 @@ export default class GitRepositoryAsync { isProjectAtRoot () { if (!this.project) return Promise.resolve(false) - if (this.projectAtRoot) { - return this.projectAtRoot - } else { - this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then(repo => this.project.relativize(repo.workdir())) - }) + if (!this.projectAtRoot) { + this.projectAtRoot = this.repoPromise + .then(repo => this.project.relativize(repo.workdir())) + .then(relativePath => relativePath === '') } + + return this.projectAtRoot } // Public: Makes a path relative to the repository's working directory. From 976fe0f2cf6314d7a1c21b35d0c00dc92bc314fd Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 20:19:58 -0500 Subject: [PATCH 169/254] Linter --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a9f1f9997..95c9fc150 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -182,7 +182,7 @@ describe('GitRepositoryAsync', () => { let editor beforeEach(() => { - spyOn(atom, "confirm") + spyOn(atom, 'confirm') const workingDirPath = copyRepository() repo = new GitRepositoryAsync(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) From b9f5853729a26424ae0edaf60f2721013e9065d1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 10:05:10 -0500 Subject: [PATCH 170/254] Const. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 95c9fc150..5d157dfc8 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -17,7 +17,7 @@ function openFixture (fixture) { } function copyRepository () { - let workingDirPath = temp.mkdirSync('atom-working-dir') + const workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) return fs.realpathSync(workingDirPath) From d14603c7786d155ccd0b4d8842ebb71a2c4f5e2c Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 10:09:52 -0500 Subject: [PATCH 171/254] Use `some` instead. --- src/git-repository-async.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a87c93973..2b006a509 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -365,9 +365,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this._filterStatusesByPath(_path).then(statuses => { - return statuses.filter(status => status.isModified()).length > 0 - }) + return this._filterStatusesByPath(_path) + .then(statuses => statuses.some(status => status.isModified())) } // Public: Resolves true if the given path is new. @@ -377,9 +376,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this._filterStatusesByPath(_path).then(statuses => { - return statuses.filter(status => status.isNew()).length > 0 - }) + return this._filterStatusesByPath(_path) + .then(statuses => statuses.some(status => status.isNew())) } // Public: Is the given path ignored? From 5d87002b3f07687f71186baec75b5acc98bc7cc1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 10:18:58 -0500 Subject: [PATCH 172/254] Use the functions. --- src/git-repository-async.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 2b006a509..1da9738a3 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -408,13 +408,9 @@ export default class GitRepositoryAsync { }) .then(statuses => { return Promise.all(statuses.map(s => s.statusBit())).then(bits => { - let directoryStatus = 0 - const filteredBits = bits.filter(b => b > 0) - if (filteredBits.length > 0) { - filteredBits.forEach(bit => directoryStatus |= bit) - } - - return directoryStatus + return bits + .filter(b => b > 0) + .reduce((status, bit) => status | bit, 0) }) }) } From c1cf5583b4d80fea4bf9a838be554a0f16107cff Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 14:48:11 -0500 Subject: [PATCH 173/254] If there isn't an index entry then it's not a submodule. --- src/git-repository-async.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1da9738a3..4e900bc16 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -233,6 +233,8 @@ export default class GitRepositoryAsync { // TODO: This'll probably be wrong if the submodule doesn't exist in the // index yet? Is that a thing? const entry = index.getByPath(_path) + if (!entry) return false + return entry.mode === submoduleMode }) } From 8a91bfb20903b95341fdc05ce43b7aeb184a4f55 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 15:34:42 -0500 Subject: [PATCH 174/254] Be consistent in our path relativization. --- src/git-repository-async.js | 38 +++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4e900bc16..75c387b69 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -153,6 +153,21 @@ export default class GitRepositoryAsync { } // Public: Makes a path relative to the repository's working directory. + // + // * `path` The {String} path to relativize. + // + // Returns a {Promise} which resolves to the relative {String} path. + relativizeToWorkingDirectory (_path) { + return this.repoPromise + .then(repo => this.relativize(_path, repo.workdir())) + } + + // Public: Makes a path relative to the repository's working directory. + // + // * `path` The {String} path to relativize. + // * `workingDirectory` The {String} working directory path. + // + // Returns the relative {String} path. relativize (_path, workingDirectory) { // Cargo-culted from git-utils. The original implementation also handles // this.openedWorkingDirectory, which is set by git-utils when the @@ -229,10 +244,11 @@ export default class GitRepositoryAsync { isSubmodule (_path) { return this.repoPromise .then(repo => repo.openIndex()) - .then(index => { + .then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)])) + .then(([index, relativePath]) => { // TODO: This'll probably be wrong if the submodule doesn't exist in the // index yet? Is that a thing? - const entry = index.getByPath(_path) + const entry = index.getByPath(relativePath) if (!entry) return false return entry.mode === submoduleMode @@ -390,7 +406,10 @@ export default class GitRepositoryAsync { // is ignored. isPathIgnored (_path) { return this.repoPromise - .then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + .then(repo => { + const relativePath = this.relativize(_path, repo.workdir()) + return Git.Ignore.pathIsIgnored(repo, relativePath) + }) .then(ignored => Boolean(ignored)) } @@ -464,8 +483,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a status {Number} or null if the // path is not in the cache. getCachedPathStatus (_path) { - return this.repoPromise - .then(repo => this.relativize(_path, repo.workdir())) + return this.relativizeToWorkingDirectory(_path) .then(relativePath => this.pathStatusCache[relativePath]) } @@ -535,7 +553,7 @@ export default class GitRepositoryAsync { .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) .then(([repo, tree]) => { const options = new Git.DiffOptions() - options.pathspec = _path + options.pathspec = this.relativize(_path, repo.workdir()) return Git.Diff.treeToWorkdir(repo, tree, options) }) .then(diff => this._getDiffLines(diff)) @@ -565,9 +583,13 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { + let relativePath = null return this.repoPromise - .then(repo => repo.getHeadCommit()) - .then(commit => commit.getEntry(_path)) + .then(repo => { + relativePath = this.relativize(_path, repo.workdir()) + return repo.getHeadCommit() + }) + .then(commit => commit.getEntry(relativePath)) .then(entry => entry.getBlob()) .then(blob => { const options = new Git.DiffOptions() From 27afc76455aeeb0900dc4e64d0cded44e5dc0203 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 20:27:10 -0500 Subject: [PATCH 175/254] Fix hunk callback position. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 75c387b69..807fadb1f 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -727,7 +727,7 @@ export default class GitRepositoryAsync { const hunkCallback = (delta, hunk, payload) => { hunks.push(hunk) } - return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null) + return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null) .then(_ => hunks) } From c4ba2132fa90704c6654a5d25d34aad30a459995 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 23:29:01 -0500 Subject: [PATCH 176/254] Accumulate the raw data we want instead of keeping hunks around. --- src/git-repository-async.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 807fadb1f..5372c7a44 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -601,18 +601,6 @@ export default class GitRepositoryAsync { } return this._diffBlobToBuffer(blob, text, options) }) - .then(hunks => { - // TODO: The old implementation just takes into account the last hunk. - // That's probably safe for most cases but maybe wrong in some edge - // cases? - const hunk = hunks[hunks.length - 1] - return { - oldStart: hunk.oldStart(), - newStart: hunk.newStart(), - oldLines: hunk.oldLines(), - newLines: hunk.newLines() - } - }) } // Checking Out @@ -721,12 +709,23 @@ export default class GitRepositoryAsync { // * `buffer` The {String} buffer. // * `options` The {NodeGit.DiffOptions} // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. + // Returns a {Promise} which resolves to an {Array} of {Object}s which have + // the following keys: + // * `oldStart` The {Number} of the old starting line. + // * `newStart` The {Number} of the new starting line. + // * `oldLines` The {Number} of old lines. + // * `newLines` The {Number} of new lines. _diffBlobToBuffer (blob, buffer, options) { const hunks = [] const hunkCallback = (delta, hunk, payload) => { - hunks.push(hunk) + hunks.push({ + oldStart: hunk.oldStart(), + newStart: hunk.newStart(), + oldLines: hunk.oldLines(), + newLines: hunk.newLines() + }) } + return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null) .then(_ => hunks) } From 36f13e603692d04db65db7422c17be1c448e8c03 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 23:29:08 -0500 Subject: [PATCH 177/254] #nocontext --- src/git-repository-async.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 5372c7a44..b26e2f097 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -593,6 +593,7 @@ export default class GitRepositoryAsync { .then(entry => entry.getBlob()) .then(blob => { const options = new Git.DiffOptions() + options.contextLines = 0 if (process.platform === 'win32') { // Ignore eol of line differences on windows so that files checked in // as LF don't report every line modified when the text contains CRLF From 4024ea956c72f5239545a0485ab6d4d4fd039ebe Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 23:58:04 -0500 Subject: [PATCH 178/254] Fix the test. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 5d157dfc8..e40c0a114 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -610,7 +610,7 @@ describe('GitRepositoryAsync', () => { }) it('returns the old and new lines of the diff', async () => { - const {oldStart, newStart, oldLines, newLines} = await repo.getLineDiffs('a.txt', 'hi there') + const [{oldStart, newStart, oldLines, newLines}] = await repo.getLineDiffs('a.txt', 'hi there') expect(oldStart).toBe(0) expect(oldLines).toBe(0) expect(newStart).toBe(1) From 1c01d05440e42c84c300a80066c126ae03f0effa Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 09:00:52 -0500 Subject: [PATCH 179/254] Try running core specs on appveyor. --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 8e33549c7..e63b9eceb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,6 +18,7 @@ environment: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% + - SET ATOM_SPECS_TASK=core - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM build_script: From d98bbb1119fef552b182476e0e31019d047e031b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 10:09:20 -0500 Subject: [PATCH 180/254] Enable tests on windows. --- build/Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 26d9c2f42..62964a3bd 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -290,7 +290,7 @@ module.exports = (grunt) -> ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI ciTasks.push('create-windows-installer:installer') if process.platform is 'win32' - ciTasks.push('test') if process.platform is 'darwin' + ciTasks.push('test') if process.platform is 'darwin' or process.platform is 'win32' ciTasks.push('codesign:installer') if process.platform is 'win32' and not process.env.CI ciTasks.push('codesign:app') if process.platform is 'darwin' and not process.env.CI ciTasks.push('publish-build') unless process.env.CI From 013fcf16f2bb4cce2797e43971ecff76043faf69 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 17:14:19 -0500 Subject: [PATCH 181/254] Revert "Enable tests on windows." This reverts commit d98bbb1119fef552b182476e0e31019d047e031b. --- build/Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 62964a3bd..26d9c2f42 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -290,7 +290,7 @@ module.exports = (grunt) -> ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI ciTasks.push('create-windows-installer:installer') if process.platform is 'win32' - ciTasks.push('test') if process.platform is 'darwin' or process.platform is 'win32' + ciTasks.push('test') if process.platform is 'darwin' ciTasks.push('codesign:installer') if process.platform is 'win32' and not process.env.CI ciTasks.push('codesign:app') if process.platform is 'darwin' and not process.env.CI ciTasks.push('publish-build') unless process.env.CI From 3f6447774082535a505844365522113f8d2ac205 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 17:14:22 -0500 Subject: [PATCH 182/254] Revert "Try running core specs on appveyor." This reverts commit 1c01d05440e42c84c300a80066c126ae03f0effa. --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e63b9eceb..8e33549c7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,6 @@ environment: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - - SET ATOM_SPECS_TASK=core - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM build_script: From 5c77effc207fbe383a95b4b01c28a491d5e67039 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 17:23:23 -0500 Subject: [PATCH 183/254] Always set the architecture to ia32 on Windows. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 8314b9cb0..7e5b534c7 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -82,7 +82,7 @@ function bootstrap() { // apm ships with 32-bit node so make sure its native modules are compiled // for a 32-bit target architecture - if (process.env.JANKY_SHA1 && process.platform === 'win32') + if (process.platform === 'win32') apmInstallCommand += ' --arch=ia32'; var commands = [ From ed92db1f43b573c25669b3d68247e7fd837bf26f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:05:29 -0500 Subject: [PATCH 184/254] Provide messages for all these steps. --- script/bootstrap | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 7e5b534c7..220c0b4f3 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -96,9 +96,18 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - apmPath + ' clean' + apmFlags, - moduleInstallCommand, - dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + { + command: apmPath + ' clean' + apmFlags, + message: 'Deleting old packages...' + }, + { + command: moduleInstallCommand, + message: 'Installing modules...', + }, + { + command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + message: 'Deduping modules...' + } ]; process.chdir(path.dirname(__dirname)); From 8ac72db0b2d8191854f698a05c1c88100fd6220e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:09:38 -0500 Subject: [PATCH 185/254] Get the Electron version from our package.json. --- .npmrc | 3 --- script/bootstrap | 11 +++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.npmrc b/.npmrc index 6fc4bd7ae..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -target = 0.34.5 -runtime = electron -disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 220c0b4f3..ed4411272 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,6 +4,7 @@ var fs = require('fs'); var verifyRequirements = require('./utils/verify-requirements'); var safeExec = require('./utils/child-process-wrapper.js').safeExec; var path = require('path'); +var _ = require('underscore-plus'); // Executes an array of commands one by one. function executeCommands(commands, done, index) { @@ -71,6 +72,15 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; + var electronVersion = require('../package.json').electronVersion; + var moduleInstallOptions = {env: _.extend({}, process.env, + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + npm_config_target: electronVersion, + npm_config_runtime: 'electron', + npm_config_disturl: 'https://atom.io/download/atom-shell' + )} + if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; @@ -103,6 +113,7 @@ function bootstrap() { { command: moduleInstallCommand, message: 'Installing modules...', + options: moduleInstallOptions }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), From 11180cea1c77544f4a975a1bf573a95ccce237db Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:11:33 -0500 Subject: [PATCH 186/254] Syntax --- script/bootstrap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index ed4411272..930c5bcd0 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -73,13 +73,13 @@ function bootstrap() { var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; var electronVersion = require('../package.json').electronVersion; - var moduleInstallOptions = {env: _.extend({}, process.env, + var moduleInstallOptions = {env: _.extend({}, process.env, { // `node-pre-gyp` will look for these when determining which binary to // download or how to rebuild. npm_config_target: electronVersion, npm_config_runtime: 'electron', npm_config_disturl: 'https://atom.io/download/atom-shell' - )} + })} if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; From 71bf3a7546cd439b14da3db435ed699ab633ea4e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:16:22 -0500 Subject: [PATCH 187/254] How about this. --- script/bootstrap | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 930c5bcd0..e6111db5c 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,7 +4,6 @@ var fs = require('fs'); var verifyRequirements = require('./utils/verify-requirements'); var safeExec = require('./utils/child-process-wrapper.js').safeExec; var path = require('path'); -var _ = require('underscore-plus'); // Executes an array of commands one by one. function executeCommands(commands, done, index) { @@ -73,13 +72,15 @@ function bootstrap() { var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; var electronVersion = require('../package.json').electronVersion; - var moduleInstallOptions = {env: _.extend({}, process.env, { - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - npm_config_target: electronVersion, - npm_config_runtime: 'electron', - npm_config_disturl: 'https://atom.io/download/atom-shell' - })} + var moduleInstallEnv = {}; + for (var e in process.env) { + moduleInstallEnv[e] = process.env[e]; + } + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + moduleInstallEnv.npm_config_target = electronVersion; + moduleInstallEnv.npm_config_runtime = 'electron'; + moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; From 91ff9079d45a1d4a50c4e503da4595c04d25a5f1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:17:06 -0500 Subject: [PATCH 188/254] Just pass through the environment. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index e6111db5c..fbdfa13d2 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -114,7 +114,7 @@ function bootstrap() { { command: moduleInstallCommand, message: 'Installing modules...', - options: moduleInstallOptions + options: {env: moduleInstallEnv} }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), From 2ccbdee48fac79710ac22e034d4a0026cf16454d Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:29:28 -0500 Subject: [PATCH 189/254] These commands will log, so no need to do it ourselves. --- script/bootstrap | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index fbdfa13d2..e0f92ec7a 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -113,13 +113,9 @@ function bootstrap() { }, { command: moduleInstallCommand, - message: 'Installing modules...', options: {env: moduleInstallEnv} }, - { - command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), - message: 'Deduping modules...' - } + command: dedupeApmCommand + ' ' + packagesToDedupe.join(' ') ]; process.chdir(path.dirname(__dirname)); From 9773adc8f803e2619fe581ff285be40acac0260d Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 14:28:50 -0500 Subject: [PATCH 190/254] Not an object anymore. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index e0f92ec7a..759b32728 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -115,7 +115,7 @@ function bootstrap() { command: moduleInstallCommand, options: {env: moduleInstallEnv} }, - command: dedupeApmCommand + ' ' + packagesToDedupe.join(' ') + dedupeApmCommand + ' ' + packagesToDedupe.join(' ') ]; process.chdir(path.dirname(__dirname)); From f23d9330fa159b75dfef88cfdfc976c4d3890578 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 14:39:02 -0500 Subject: [PATCH 191/254] Try passing them as args. --- script/bootstrap | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 759b32728..3c517f509 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -68,19 +68,10 @@ function bootstrap() { var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; var apmInstallOptions = {cwd: apmInstallPath}; - var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; var electronVersion = require('../package.json').electronVersion; - var moduleInstallEnv = {}; - for (var e in process.env) { - moduleInstallEnv[e] = process.env[e]; - } - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - moduleInstallEnv.npm_config_target = electronVersion; - moduleInstallEnv.npm_config_runtime = 'electron'; - moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; + var moduleInstallCommand = apmPath + ' install ' + '--target=' + electronVersion + ' --runtime=electron --disturl=https://atom.io/download/atom-shell ' + apmFlags; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; @@ -113,7 +104,6 @@ function bootstrap() { }, { command: moduleInstallCommand, - options: {env: moduleInstallEnv} }, dedupeApmCommand + ' ' + packagesToDedupe.join(' ') ]; From be9d74adbfd295aeb3d0ff2314acdb0d1c9df5e4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 15:10:01 -0500 Subject: [PATCH 192/254] Back to using npmrc. --- .npmrc | 3 +++ script/bootstrap | 15 ++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.npmrc b/.npmrc index c5ff09782..6fc4bd7ae 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +target = 0.34.5 +runtime = electron +disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 3c517f509..7e5b534c7 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -68,11 +68,9 @@ function bootstrap() { var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; var apmInstallOptions = {cwd: apmInstallPath}; + var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; - var electronVersion = require('../package.json').electronVersion; - var moduleInstallCommand = apmPath + ' install ' + '--target=' + electronVersion + ' --runtime=electron --disturl=https://atom.io/download/atom-shell ' + apmFlags; - if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; @@ -98,14 +96,9 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - { - command: apmPath + ' clean' + apmFlags, - message: 'Deleting old packages...' - }, - { - command: moduleInstallCommand, - }, - dedupeApmCommand + ' ' + packagesToDedupe.join(' ') + apmPath + ' clean' + apmFlags, + moduleInstallCommand, + dedupeApmCommand + ' ' + packagesToDedupe.join(' '), ]; process.chdir(path.dirname(__dirname)); From 1eceae96cc4172ea769dad4aac17f1005b855e1b Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 15:42:19 -0500 Subject: [PATCH 193/254] Once more, with feeling. --- .npmrc | 3 --- script/bootstrap | 27 ++++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.npmrc b/.npmrc index 6fc4bd7ae..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -target = 0.34.5 -runtime = electron -disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 7e5b534c7..fc2df36bd 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -71,6 +71,17 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; + var moduleInstallEnv = {}; + for (var e in process.env) { + moduleInstallEnv[e] = process.env[e]; + } + + var electronVersion = require('../package.json').electronVersion; + moduleInstallEnv.npm_config_target = electronVersion; + moduleInstallEnv.npm_config_runtime = 'electron'; + moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; + var moduleInstallOptions = {env: moduleInstallEnv}; + if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; @@ -96,9 +107,19 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - apmPath + ' clean' + apmFlags, - moduleInstallCommand, - dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + { + command: apmPath + ' clean' + apmFlags, + message: 'Deleting old modules...', + options: moduleInstallOptions + }, + { + command: moduleInstallCommand, + options: moduleInstallOptions + }, + { + command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + options: moduleInstallOptions + } ]; process.chdir(path.dirname(__dirname)); From bf0f6afe20aae0472b71accd05736fc165b6d9e2 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 16:18:29 -0500 Subject: [PATCH 194/254] Log the environment. --- script/bootstrap | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index fc2df36bd..983d0f033 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -73,7 +73,7 @@ function bootstrap() { var moduleInstallEnv = {}; for (var e in process.env) { - moduleInstallEnv[e] = process.env[e]; + // moduleInstallEnv[e] = process.env[e]; } var electronVersion = require('../package.json').electronVersion; @@ -82,6 +82,9 @@ function bootstrap() { moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; var moduleInstallOptions = {env: moduleInstallEnv}; + console.log('env:') + console.log(moduleInstallEnv) + if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; From a7ba6c86386168c9b893977567856553dfdcc347 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 16:21:37 -0500 Subject: [PATCH 195/254] Try args again. --- script/bootstrap | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 983d0f033..dfeb47b9d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -71,19 +71,8 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; - var moduleInstallEnv = {}; - for (var e in process.env) { - // moduleInstallEnv[e] = process.env[e]; - } - var electronVersion = require('../package.json').electronVersion; - moduleInstallEnv.npm_config_target = electronVersion; - moduleInstallEnv.npm_config_runtime = 'electron'; - moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; - var moduleInstallOptions = {env: moduleInstallEnv}; - - console.log('env:') - console.log(moduleInstallEnv) + apmFlags += ' --target=' + electronVersion + ' --runtime=electron --dist-url=https://atom.io/download/atom-shell'; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; @@ -113,15 +102,12 @@ function bootstrap() { { command: apmPath + ' clean' + apmFlags, message: 'Deleting old modules...', - options: moduleInstallOptions }, { command: moduleInstallCommand, - options: moduleInstallOptions }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), - options: moduleInstallOptions } ]; From 666958d6b93832de568e790982a3beff801cb4c0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 16:50:00 -0500 Subject: [PATCH 196/254] Try just setting the target? --- .npmrc | 2 ++ script/bootstrap | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index c5ff09782..9fa375bb0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ cache = ~/.atom/.npm +runtime = electron +disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index dfeb47b9d..b01f033c8 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -71,8 +71,14 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; + var moduleInstallEnv = {}; + for (var e in process.env) { + moduleInstallEnv[e] = process.env[e]; + } + var electronVersion = require('../package.json').electronVersion; - apmFlags += ' --target=' + electronVersion + ' --runtime=electron --dist-url=https://atom.io/download/atom-shell'; + moduleInstallEnv.npm_config_target = electronVersion; + var moduleInstallOptions = {env: moduleInstallEnv}; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; @@ -102,12 +108,15 @@ function bootstrap() { { command: apmPath + ' clean' + apmFlags, message: 'Deleting old modules...', + options: moduleInstallOptions }, { command: moduleInstallCommand, + options: moduleInstallOptions }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + options: moduleInstallOptions } ]; From f04fee2ad9d52178d5e53e34ddcece7f394dedc3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 17:23:44 -0500 Subject: [PATCH 197/254] Comment why. --- script/bootstrap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/bootstrap b/script/bootstrap index b01f033c8..2872d1141 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -76,6 +76,8 @@ function bootstrap() { moduleInstallEnv[e] = process.env[e]; } + // Set our target (Electron) version so that node-pre-gyp can download the + // proper binaries. var electronVersion = require('../package.json').electronVersion; moduleInstallEnv.npm_config_target = electronVersion; var moduleInstallOptions = {env: moduleInstallEnv}; From f4138018364161a0821c3526d35ee0b8d0c0d3f1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 29 Dec 2015 17:10:01 -0500 Subject: [PATCH 198/254] Remove items that don't have any changes. --- src/git-repository-async.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b26e2f097..8c7364fe9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -458,7 +458,12 @@ export default class GitRepositoryAsync { const cachedStatus = this.pathStatusCache[relativePath] || 0 const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { - this.pathStatusCache[relativePath] = status + if (status === Git.Status.STATUS.CURRENT) { + delete this.pathStatusCache[relativePath] + } else { + this.pathStatusCache[relativePath] = status + } + this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } From 34ab7983620911ad2b578ebafa3d8cb5217841aa Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 29 Dec 2015 17:10:09 -0500 Subject: [PATCH 199/254] Added getCachedPathStatuses() --- src/git-repository-async.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8c7364fe9..c8837e893 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -492,6 +492,14 @@ export default class GitRepositoryAsync { .then(relativePath => this.pathStatusCache[relativePath]) } + // Public: Get the cached statuses for the repository. + // + // Returns an {Object} of {Number} statuses, keyed by {String} working + // directory-relative file names. + getCachedPathStatuses () { + return this.pathStatusCache + } + // Public: Returns true if the given status indicates modification. // // * `statusBit` A {Number} representing the status. From 3af5e8cc1882418ab9b732ed249d2f96cf446852 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 30 Dec 2015 12:10:33 -0500 Subject: [PATCH 200/254] Fix path standardization on case-sensitive file systems. --- src/git-repository-async.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index c8837e893..d3ba9897b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -194,17 +194,16 @@ export default class GitRepositoryAsync { workingDirectory = `${workingDirectory}/` } + const originalPath = _path if (this.isCaseInsensitive) { - const lowerCasePath = _path.toLowerCase() - + _path = _path.toLowerCase() workingDirectory = workingDirectory.toLowerCase() - if (lowerCasePath.indexOf(workingDirectory) === 0) { - return _path.substring(workingDirectory.length) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } + } + + if (_path.indexOf(workingDirectory) === 0) { + return originalPath.substring(workingDirectory.length) + } else if (_path === workingDirectory) { + return '' } return _path From 28326b3674b183fc425c7c220152fc92069f7d65 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 30 Dec 2015 12:11:06 -0500 Subject: [PATCH 201/254] Strip any leading /private/. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I don’t love that we have to do this manually, but I also can’t find any node function that’ll do it for us :( --- src/git-repository-async.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d3ba9897b..f15bdf439 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -182,6 +182,11 @@ export default class GitRepositoryAsync { return _path } + // Depending on where the paths come from, they may have a '/private/' + // prefix. Standardize by stripping that out. + _path = _path.replace(/^\/private\//, '/') + workingDirectory = workingDirectory.replace(/^\/private\//, '/') + if (process.platform === 'win32') { _path = _path.replace(/\\/g, '/') } else { From 07eebdccb857f81b65c2299ed533e9fa7728cfb8 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 11:26:58 -0500 Subject: [PATCH 202/254] Use .openExt instead of .open. Use openExt to open the repository so we get the same repo-searching behavior we had with git-utils. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f15bdf439..fe4408685 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -34,7 +34,7 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.open(_path) + this.repoPromise = Git.Repository.openExt(_path) this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} From 73cb867ccd3f734cbd4fa6aec682c32c164710dc Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:14:08 -0500 Subject: [PATCH 203/254] Do proper path-filtering for status. --- src/git-repository-async.js | 51 ++++++++++++++----------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fe4408685..9eb643cc8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -387,7 +387,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this._filterStatusesByPath(_path) + return this._getStatus([_path]) .then(statuses => statuses.some(status => status.isModified())) } @@ -398,7 +398,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this._filterStatusesByPath(_path) + return this._getStatus([_path]) .then(statuses => statuses.some(status => status.isNew())) } @@ -425,11 +425,10 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - // XXX _filterSBD already gets repoPromise return this.repoPromise .then(repo => { const relativePath = this.relativize(directoryPath, repo.workdir()) - return this._filterStatusesByDirectory(relativePath) + return this._getStatus([relativePath]) }) .then(statuses => { return Promise.all(statuses.map(s => s.statusBit())).then(bits => { @@ -456,7 +455,7 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => { relativePath = this.relativize(_path, repo.workdir()) - return this._filterStatusesByPath(_path) + return this._getStatus([relativePath]) }) .then(statuses => { const cachedStatus = this.pathStatusCache[relativePath] || 0 @@ -778,7 +777,6 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { - // update the status cache const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) return Promise.all(statusPairs) .then(statusesByPath => _.object(statusesByPath)) @@ -873,36 +871,25 @@ export default class GitRepositoryAsync { this.subscriptions.add(bufferSubscriptions) } - // Get the status for the given path. + // Get the status for the given paths. // - // * `path` The {String} path whose status is wanted. - // - // Returns a {Promise} which resolves to the {NodeGit.StatusFile} status for - // the path. - _filterStatusesByPath (_path) { - // TODO: Is there a more efficient way to do this? - let basePath = null - return this.repoPromise - .then(repo => { - basePath = repo.workdir() - return repo.getStatus() - }) - .then(statuses => { - return statuses.filter(status => _path === path.join(basePath, status.path())) - }) - } - - // Get the status for everything in the given directory. - // - // * `directoryPath` The {String} directory whose status is wanted. + // * `paths` The {String} paths whose status is wanted. If undefined, get the + // status for the whole repository. // // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} - // statuses for every file in the directory. - _filterStatusesByDirectory (directoryPath) { + // statuses for the paths. + _getStatus (paths) { return this.repoPromise - .then(repo => repo.getStatus()) - .then(statuses => { - return statuses.filter(status => status.path().indexOf(directoryPath) === 0) + .then(repo => { + const opts = { + flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH + } + + if (paths) { + opts.pathspec = paths + } + + return repo.getStatus(opts) }) } } From d8a5418f1e068370397c7571399bb06e378bb336 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:14:24 -0500 Subject: [PATCH 204/254] Only refresh the status for the open project. --- src/git-repository-async.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 9eb643cc8..bfe3bb1ab 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -774,8 +774,14 @@ export default class GitRepositoryAsync { _refreshStatus () { this._refreshingCount++ - return this.repoPromise - .then(repo => repo.getStatus()) + const projectPathsPromises = this.project.getPaths() + .map(p => this.relativizeToWorkingDirectory(p)) + + Promise.all(projectPathsPromises) + .then(paths => paths.filter(p => p.length > 0)) + .then(projectPaths => { + return this._getStatus(projectPaths.length > 0 ? projectPaths : null) + }) .then(statuses => { const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) return Promise.all(statusPairs) From d9b39323cf01273e240cccbf0dab024416856e55 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:20:54 -0500 Subject: [PATCH 205/254] Fix the openExt call. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index bfe3bb1ab..9f414ecf9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -34,7 +34,7 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.openExt(_path) + this.repoPromise = Git.Repository.openExt(_path, 0, '') this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} From 1a35545bd70022eb348c2878c4d97981b2ed648e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:48:29 -0500 Subject: [PATCH 206/254] _destroyed => _isDestroyed --- src/git-repository-async.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 9f414ecf9..658d3ddbd 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -780,6 +780,8 @@ export default class GitRepositoryAsync { Promise.all(projectPathsPromises) .then(paths => paths.filter(p => p.length > 0)) .then(projectPaths => { + if (this._isDestroyed()) return [] + return this._getStatus(projectPaths.length > 0 ? projectPaths : null) }) .then(statuses => { @@ -818,7 +820,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the {NodeGit.Repository}. getRepo (_path) { - if (this._destroyed()) { + if (this._isDestroyed()) { const error = new Error('Repository has been destroyed') error.name = GitRepositoryAsync.DestroyedErrorName return Promise.reject(error) @@ -849,7 +851,7 @@ export default class GitRepositoryAsync { // Has the repository been destroyed? // // Returns a {Boolean}. - _destroyed () { + _isDestroyed () { return this.repoPromise == null } From e3451090ed45b5b7122417dbe5409a051ddc29ec Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:48:39 -0500 Subject: [PATCH 207/254] We might not have a project. --- src/git-repository-async.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 658d3ddbd..394a158ba 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -774,8 +774,11 @@ export default class GitRepositoryAsync { _refreshStatus () { this._refreshingCount++ - const projectPathsPromises = this.project.getPaths() - .map(p => this.relativizeToWorkingDirectory(p)) + let projectPathsPromises = Promise.resolve([]) + if (this.project) { + projectPathsPromises = this.project.getPaths() + .map(p => this.relativizeToWorkingDirectory(p)) + } Promise.all(projectPathsPromises) .then(paths => paths.filter(p => p.length > 0)) From 5a7b4562e19adcc606d3fa75d1d886be40f41dbd Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:14:02 -0500 Subject: [PATCH 208/254] Use a default parameter. --- src/git-repository-async.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 394a158ba..430423ac8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -29,7 +29,7 @@ export default class GitRepositoryAsync { return 'GitRepositoryAsync.destroyed' } - constructor (_path, options) { + constructor (_path, options = {}) { this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() @@ -40,8 +40,6 @@ export default class GitRepositoryAsync { this._refreshingCount = 0 - options = options || {} - let {refreshOnWindowFocus = true} = options if (refreshOnWindowFocus) { const onWindowFocus = () => this.refreshStatus() From 60fc0acb715132bf7d70244bfb5db45585f20d1f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:25:51 -0500 Subject: [PATCH 209/254] Relativize these paths too. --- src/git-repository-async.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 430423ac8..d9a0c3214 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -385,7 +385,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this._getStatus([_path]) + return this.relativizeToWorkingDirectory(_path) + .then(relativePath => this._getStatus([relativePath])) .then(statuses => statuses.some(status => status.isModified())) } @@ -396,7 +397,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this._getStatus([_path]) + return this.relativizeToWorkingDirectory(_path) + .then(relativePath => this._getStatus([relativePath])) .then(statuses => statuses.some(status => status.isNew())) } From 843442a74178e23a5cbf4175b17d894dc269a1dd Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:25:59 -0500 Subject: [PATCH 210/254] So much depends upon a return statement. --- src/git-repository-async.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d9a0c3214..fffd443a9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -774,17 +774,15 @@ export default class GitRepositoryAsync { _refreshStatus () { this._refreshingCount++ - let projectPathsPromises = Promise.resolve([]) + let projectPathsPromises = [Promise.resolve('')] if (this.project) { projectPathsPromises = this.project.getPaths() .map(p => this.relativizeToWorkingDirectory(p)) } - Promise.all(projectPathsPromises) + return Promise.all(projectPathsPromises) .then(paths => paths.filter(p => p.length > 0)) .then(projectPaths => { - if (this._isDestroyed()) return [] - return this._getStatus(projectPaths.length > 0 ? projectPaths : null) }) .then(statuses => { From 3752f6616021a5c2caf7a42950719af5ab74d406 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:27:55 -0500 Subject: [PATCH 211/254] Use .getRepo everywhere. --- src/git-repository-async.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fffd443a9..1bc0c1684 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -127,13 +127,13 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to the {String} path of the // repository. getPath () { - return this.repoPromise.then(repo => repo.path().replace(/\/$/, '')) + return this.getRepo().then(repo => repo.path().replace(/\/$/, '')) } // Public: Returns a {Promise} which resolves to the {String} working // directory path of the repository. getWorkingDirectory () { - return this.repoPromise.then(repo => repo.workdir()) + return this.getRepo().then(repo => repo.workdir()) } // Public: Returns a {Promise} that resolves to true if at the root, false if @@ -142,7 +142,7 @@ export default class GitRepositoryAsync { if (!this.project) return Promise.resolve(false) if (!this.projectAtRoot) { - this.projectAtRoot = this.repoPromise + this.projectAtRoot = this.getRepo() .then(repo => this.project.relativize(repo.workdir())) .then(relativePath => relativePath === '') } @@ -156,7 +156,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the relative {String} path. relativizeToWorkingDirectory (_path) { - return this.repoPromise + return this.getRepo() .then(repo => this.relativize(_path, repo.workdir())) } @@ -215,7 +215,7 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to whether the given branch // exists. hasBranch (branch) { - return this.repoPromise + return this.getRepo() .then(repo => repo.getBranch(branch)) .then(branch => branch != null) .catch(_ => false) @@ -244,7 +244,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.repoPromise + return this.getRepo() .then(repo => repo.openIndex()) .then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)])) .then(([index, relativePath]) => { @@ -409,7 +409,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return this.repoPromise + return this.getRepo() .then(repo => { const relativePath = this.relativize(_path, repo.workdir()) return Git.Ignore.pathIsIgnored(repo, relativePath) @@ -425,7 +425,7 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - return this.repoPromise + return this.getRepo() .then(repo => { const relativePath = this.relativize(directoryPath, repo.workdir()) return this._getStatus([relativePath]) @@ -452,7 +452,7 @@ export default class GitRepositoryAsync { this._refreshingCount++ let relativePath - return this.repoPromise + return this.getRepo() .then(repo => { relativePath = this.relativize(_path, repo.workdir()) return this._getStatus([relativePath]) @@ -564,7 +564,7 @@ export default class GitRepositoryAsync { // * `added` The {Number} of added lines. // * `deleted` The {Number} of deleted lines. getDiffStats (_path) { - return this.repoPromise + return this.getRepo() .then(repo => Promise.all([repo, repo.getHeadCommit()])) .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) .then(([repo, tree]) => { @@ -600,7 +600,7 @@ export default class GitRepositoryAsync { // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { let relativePath = null - return this.repoPromise + return this.getRepo() .then(repo => { relativePath = this.relativize(_path, repo.workdir()) return repo.getHeadCommit() @@ -638,7 +638,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves or rejects depending on whether the // method was successful. checkoutHead (_path) { - return this.repoPromise + return this.getRepo() .then(repo => { const checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this.relativize(_path, repo.workdir())] @@ -656,7 +656,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - return this.repoPromise + return this.getRepo() .then(repo => repo.checkoutBranch(reference)) .catch(error => { if (create) { @@ -693,7 +693,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the // created branch. _createBranch (name) { - return this.repoPromise + return this.getRepo() .then(repo => Promise.all([repo, repo.getHeadCommit()])) .then(([repo, commit]) => repo.createBranch(name, commit)) } @@ -751,7 +751,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the {String} branch name. _refreshBranch () { - return this.repoPromise + return this.getRepo() .then(repo => repo.getCurrentBranch()) .then(ref => ref.name()) .then(branchName => this.branch = branchName) @@ -888,7 +888,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} // statuses for the paths. _getStatus (paths) { - return this.repoPromise + return this.getRepo() .then(repo => { const opts = { flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH From a2ab84c3e6bd997ba07ebb86f3fa1176c63c5765 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:36:13 -0500 Subject: [PATCH 212/254] Catch errors that are thrown while we're refreshing. --- src/git-repository-async.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1bc0c1684..95bb407d8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -810,7 +810,18 @@ export default class GitRepositoryAsync { const branch = this._refreshBranch() const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName)) - return Promise.all([status, branch, aheadBehind]).then(_ => null) + return Promise.all([status, branch, aheadBehind]) + .then(_ => null) + // Because all these refresh steps happen asynchronously, it's entirely + // possible the repository was destroyed while we were working. In which + // case we should just swallow the error. + .catch(e => { + if (this._isDestroyed()) { + return null + } else { + return Promise.reject(e) + } + }) } // Get the NodeGit repository for the given path. From b234f8481ddc9ff218f48ddc70a086830b6d5771 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 16:12:22 -0500 Subject: [PATCH 213/254] We don't actually use `repo`. --- spec/git-repository-async-spec.js | 3 +-- src/git-repository-async.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e40c0a114..bbaa7ef75 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -31,7 +31,7 @@ describe('GitRepositoryAsync', () => { }) describe('@open(path)', () => { - it('repo is null when no repository is found', async () => { + it('should throw when no repository is found', async () => { repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) let threw = false @@ -42,7 +42,6 @@ describe('GitRepositoryAsync', () => { } expect(threw).toBe(true) - expect(repo.repo).toBe(null) }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 95bb407d8..035af9945 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -30,7 +30,6 @@ export default class GitRepositoryAsync { } constructor (_path, options = {}) { - this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} From a63e30362fe87834aaba0ef53b0f0e7eb083fdb4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 16:16:50 -0500 Subject: [PATCH 214/254] Documentation for .DestroyedErrorName --- src/git-repository-async.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 035af9945..295fa5ca2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -25,6 +25,8 @@ export default class GitRepositoryAsync { return Git } + // The name of the error thrown when an action is attempted on a destroyed + // repository. static get DestroyedErrorName () { return 'GitRepositoryAsync.destroyed' } From 6bf9d7eb7d16c5a6e13cbb20ed1001b353489e64 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 16:17:16 -0500 Subject: [PATCH 215/254] Added .openNodeGitRepository --- spec/git-repository-async-spec.js | 13 +++++++++++++ src/git-repository-async.js | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index bbaa7ef75..b59bc35ab 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -45,6 +45,19 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.openNodeGitRepository()', () => { + it('returns a new repository instance', async () => { + repo = openFixture('master.git') + + const originalRepo = await repo.getRepo() + expect(originalRepo).not.toBeNull() + + const nodeGitRepo = repo.openNodeGitRepository() + expect(nodeGitRepo).not.toBeNull() + expect(originalRepo).not.toBe(nodeGitRepo) + }) + }) + describe('.getPath()', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 295fa5ca2..f48988f91 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -35,7 +35,10 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.openExt(_path, 0, '') + // NB: This needs to happen before the following .openNodeGitRepository + // call. + this.openedPath = _path + this.repoPromise = this.openNodeGitRepository() this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} @@ -851,6 +854,16 @@ export default class GitRepositoryAsync { }) } + // Open a new instance of the underlying {NodeGit.Repository}. + // + // By opening multiple connections to the same underlying repository, users + // can safely access the same repository concurrently. + // + // Returns the new {NodeGit.Repository}. + openNodeGitRepository () { + return Git.Repository.openExt(this.openedPath, 0, '') + } + // Section: Private // ================ From 6b4a05495e2c5615561cdcbedd7b28613b17c2a9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 12:38:10 -0500 Subject: [PATCH 216/254] Commas are important. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 3f9b5dc46..72c12aedf 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -118,7 +118,7 @@ function bootstrap() { }, { command: apmPath + ' clean' + apmFlags, - message: 'Deleting old packages...' + message: 'Deleting old packages...', options: moduleInstallOptions }, { From 757bbae1e283784debd8d8d2ddf16eba11248ac9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 15:26:16 -0500 Subject: [PATCH 217/254] s//openNodeGitRepository/openRepository. --- spec/git-repository-async-spec.js | 4 ++-- src/git-repository-async.js | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index b59bc35ab..70260fe49 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -45,14 +45,14 @@ describe('GitRepositoryAsync', () => { }) }) - describe('.openNodeGitRepository()', () => { + describe('.openRepository()', () => { it('returns a new repository instance', async () => { repo = openFixture('master.git') const originalRepo = await repo.getRepo() expect(originalRepo).not.toBeNull() - const nodeGitRepo = repo.openNodeGitRepository() + const nodeGitRepo = repo.openRepository() expect(nodeGitRepo).not.toBeNull() expect(originalRepo).not.toBe(nodeGitRepo) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f48988f91..899675beb 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -35,10 +35,9 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - // NB: This needs to happen before the following .openNodeGitRepository - // call. + // NB: This needs to happen before the following .openRepository call. this.openedPath = _path - this.repoPromise = this.openNodeGitRepository() + this.repoPromise = this.openRepository() this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} @@ -860,7 +859,7 @@ export default class GitRepositoryAsync { // can safely access the same repository concurrently. // // Returns the new {NodeGit.Repository}. - openNodeGitRepository () { + openRepository () { return Git.Repository.openExt(this.openedPath, 0, '') } From 5045477b5dbb5c31a0954a8716591eb26b77a3b0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 15:28:48 -0500 Subject: [PATCH 218/254] path is never used. --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 899675beb..259e73f18 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -2,7 +2,6 @@ import fs from 'fs-plus' import Git from 'nodegit' -import path from 'path' import {Emitter, CompositeDisposable, Disposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE From 51bf940fb3dcbb4abbc23c9dcacd5fe724469c8c Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 15:51:34 -0500 Subject: [PATCH 219/254] :arrow_up: nodegit@0.6.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73ab5026b..72e1f892c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit": "0.6.0", + "nodegit": "0.6.3", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 26cd7c63ccbda50b5147393874623448d4d92800 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 17:10:01 -0500 Subject: [PATCH 220/254] s/upstreamByPath/upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ll track submodule status separately. --- src/git-repository-async.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 259e73f18..1c77575a6 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -38,7 +38,8 @@ export default class GitRepositoryAsync { this.openedPath = _path this.repoPromise = this.openRepository() this.isCaseInsensitive = fs.isCaseInsensitive() - this.upstreamByPath = {} + this.upstream = {} + this.submodulesByName = {} this._refreshingCount = 0 @@ -292,7 +293,8 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getCachedUpstreamAheadBehindCount (_path) { - return this.upstreamByPath[_path || '.'] + // TODO: take submodules into account + return this.upstream } // Public: Returns the git configuration value specified by the key. @@ -767,7 +769,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which will resolve to {null}. _refreshAheadBehindCount (branchName) { return this.getAheadBehindCount(branchName) - .then(counts => this.upstreamByPath['.'] = counts) + .then(counts => this.upstream = counts) } // Refresh the cached status. From eedac0a9513f14aac391605d6be7720cfb4a7ec4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 17:31:34 -0500 Subject: [PATCH 221/254] We'll store them by path instead of by name. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1c77575a6..b1daca77a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -39,7 +39,7 @@ export default class GitRepositoryAsync { this.repoPromise = this.openRepository() this.isCaseInsensitive = fs.isCaseInsensitive() this.upstream = {} - this.submodulesByName = {} + this.submodulesByPath = {} this._refreshingCount = 0 From 75fe311e22930a23fc7837defbc4906abea2428f Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 7 Jan 2016 12:19:51 -0500 Subject: [PATCH 222/254] Refresh the status for submodules too. --- src/git-repository-async.js | 85 +++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b1daca77a..7dfe378c3 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,6 +1,7 @@ 'use babel' import fs from 'fs-plus' +import path from 'path' import Git from 'nodegit' import {Emitter, CompositeDisposable, Disposable} from 'event-kit' @@ -772,12 +773,11 @@ export default class GitRepositoryAsync { .then(counts => this.upstream = counts) } - // Refresh the cached status. + // Get the status for this repository. // - // Returns a {Promise} which will resolve to {null}. - _refreshStatus () { - this._refreshingCount++ - + // Returns a {Promise} that will resolve to an object of {String} paths to the + // {Number} status. + _getRepositoryStatus () { let projectPathsPromises = [Promise.resolve('')] if (this.project) { projectPathsPromises = this.project.getPaths() @@ -791,15 +791,71 @@ export default class GitRepositoryAsync { }) .then(statuses => { const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) - return Promise.all(statusPairs) - .then(statusesByPath => _.object(statusesByPath)) + return _.object(statusPairs) }) - .then(newPathStatusCache => { - if (!_.isEqual(this.pathStatusCache, newPathStatusCache) && this.emitter != null) { + } + + // Get the status for the given submodule. + // + // * `submodule` The {NodeGit.Repository} for the submodule. + // + // Returns a {Promise} which resolves to an {Object}, keyed by {String} + // repo-relative {Number} statuses. + _getSubmoduleStatus (submodule) { + return this._getStatus(null, submodule) + .then(statuses => { + return this.getRepo().then(repo => { + const statusPairs = statuses.map(status => { + const absolutePath = path.join(submodule.workdir(), status.path()) + // Get the path relative to the containing repository. + const relativePath = this.relativize(absolutePath, repo.workdir()) + return [relativePath, status.statusBit()] + }) + return _.object(statusPairs) + }) + }) + } + + // Get the status for the submodules in the repository. + // + // Returns a {Promise} that will resolve to an object of {String} paths to the + // {Number} status. + _getSubmoduleStatuses () { + return this.getRepo() + .then(repo => Promise.all([repo, repo.getSubmoduleNames()])) + .then(([repo, submodules]) => { + return Promise.all(submodules.map(s => Git.Submodule.lookup(repo, s))) + }) + .then(submodules => submodules.map(s => s.path())) + .then(relativePaths => { + return this.getRepo().then(repo => { + const workingDirectory = repo.workdir() + return relativePaths.map(p => path.join(workingDirectory, p)) + }) + }) + .then(paths => Promise.all(paths.map(p => Git.Repository.open(p)))) + .then(repos => { + return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo))) + }) + .then(statuses => _.extend({}, ...statuses)) + .then(statuses => { + console.log(statuses) + }) + } + + // Refresh the cached status. + // + // Returns a {Promise} which will resolve to {null}. + _refreshStatus () { + this._refreshingCount++ + + return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()]) + .then(([repositoryStatus, submoduleStatus]) => { + const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus) + if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) { this.emitter.emit('did-change-statuses') } - this.pathStatusCache = newPathStatusCache - return newPathStatusCache + this.pathStatusCache = statusesByPath }) .then(_ => this._refreshingCount--) } @@ -909,11 +965,14 @@ export default class GitRepositoryAsync { // // * `paths` The {String} paths whose status is wanted. If undefined, get the // status for the whole repository. + // * `repo` The {NodeGit.Repository} the repository to get status from. If + // undefined, it will use this repository. // // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} // statuses for the paths. - _getStatus (paths) { - return this.getRepo() + _getStatus (paths, repo) { + const repoPromise = (repo ? Promise.resolve(repo) : this.getRepo()) + return repoPromise .then(repo => { const opts = { flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH From a86d3b6d94070587bb17049a2579bf19742c463e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 7 Jan 2016 12:44:56 -0500 Subject: [PATCH 223/254] Stop logging. --- src/git-repository-async.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 7dfe378c3..2e418a294 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -838,9 +838,6 @@ export default class GitRepositoryAsync { return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo))) }) .then(statuses => _.extend({}, ...statuses)) - .then(statuses => { - console.log(statuses) - }) } // Refresh the cached status. From 0bef58a8a4b155e0f8f06d4b987f32e2a845ca1c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 7 Jan 2016 13:05:36 -0500 Subject: [PATCH 224/254] Added a fixture that has submodules. --- .../git/repo-with-submodules/.gitmodules | 6 + spec/fixtures/git/repo-with-submodules/README | 0 .../You-Dont-Need-jQuery/.babelrc | 3 + .../You-Dont-Need-jQuery/.eslintrc | 18 + .../You-Dont-Need-jQuery/.gitignore | 5 + .../You-Dont-Need-jQuery/.travis.yml | 10 + .../You-Dont-Need-jQuery/LICENSE | 22 + .../You-Dont-Need-jQuery/README-es.md | 637 +++++++++ .../You-Dont-Need-jQuery/README-id.md | 634 +++++++++ .../You-Dont-Need-jQuery/README-it.md | 651 +++++++++ .../You-Dont-Need-jQuery/README-my.md | 615 +++++++++ .../You-Dont-Need-jQuery/README-ru.md | 650 +++++++++ .../You-Dont-Need-jQuery/README-tr.md | 665 +++++++++ .../You-Dont-Need-jQuery/README-vi.md | 634 +++++++++ .../You-Dont-Need-jQuery/README.ko-KR.md | 709 ++++++++++ .../You-Dont-Need-jQuery/README.md | 1190 +++++++++++++++++ .../You-Dont-Need-jQuery/README.pt-BR.md | 628 +++++++++ .../You-Dont-Need-jQuery/README.zh-CN.md | 656 +++++++++ .../You-Dont-Need-jQuery/git.git | 1 + .../You-Dont-Need-jQuery/karma.conf.js | 92 ++ .../You-Dont-Need-jQuery/package.json | 53 + .../You-Dont-Need-jQuery/test/README.md | 13 + .../You-Dont-Need-jQuery/test/css.spec.js | 1 + .../You-Dont-Need-jQuery/test/dom.spec.js | 1 + .../You-Dont-Need-jQuery/test/query.spec.js | 71 + .../test/utilities.spec.js | 1 + .../git.git/COMMIT_EDITMSG | 9 + .../git/repo-with-submodules/git.git/HEAD | 1 + .../git/repo-with-submodules/git.git/config | 11 + .../repo-with-submodules/git.git/description | 1 + .../git.git/hooks/applypatch-msg.sample | 15 + .../git.git/hooks/commit-msg.sample | 24 + .../git.git/hooks/post-update.sample | 8 + .../git.git/hooks/pre-applypatch.sample | 14 + .../git.git/hooks/pre-commit.sample | 49 + .../git.git/hooks/pre-push.sample | 53 + .../git.git/hooks/pre-rebase.sample | 169 +++ .../git.git/hooks/prepare-commit-msg.sample | 36 + .../git.git/hooks/update.sample | 128 ++ .../git/repo-with-submodules/git.git/index | Bin 0 -> 377 bytes .../repo-with-submodules/git.git/info/exclude | 6 + .../repo-with-submodules/git.git/logs/HEAD | 2 + .../git.git/logs/refs/heads/master | 2 + .../git.git/modules/You-Dont-Need-jQuery/HEAD | 1 + .../modules/You-Dont-Need-jQuery/ORIG_HEAD | 1 + .../modules/You-Dont-Need-jQuery/config | 14 + .../modules/You-Dont-Need-jQuery/description | 1 + .../modules/You-Dont-Need-jQuery/gitdir | 1 + .../hooks/applypatch-msg.sample | 15 + .../hooks/commit-msg.sample | 24 + .../hooks/post-update.sample | 8 + .../hooks/pre-applypatch.sample | 14 + .../hooks/pre-commit.sample | 49 + .../hooks/pre-push.sample | 53 + .../hooks/pre-rebase.sample | 169 +++ .../hooks/prepare-commit-msg.sample | 36 + .../You-Dont-Need-jQuery/hooks/update.sample | 128 ++ .../modules/You-Dont-Need-jQuery/index | Bin 0 -> 1919 bytes .../modules/You-Dont-Need-jQuery/info/exclude | 6 + .../modules/You-Dont-Need-jQuery/logs/HEAD | 1 + .../logs/refs/heads/master | 1 + .../logs/refs/remotes/origin/HEAD | 1 + ...8b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx | Bin 0 -> 18152 bytes ...b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack | Bin 0 -> 202626 bytes .../modules/You-Dont-Need-jQuery/packed-refs | 2 + .../You-Dont-Need-jQuery/refs/heads/master | 1 + .../refs/remotes/origin/HEAD | 1 + .../git.git/modules/jstips/HEAD | 1 + .../git.git/modules/jstips/ORIG_HEAD | 1 + .../git.git/modules/jstips/config | 14 + .../git.git/modules/jstips/description | 1 + .../git.git/modules/jstips/gitdir | 1 + .../jstips/hooks/applypatch-msg.sample | 15 + .../modules/jstips/hooks/commit-msg.sample | 24 + .../modules/jstips/hooks/post-update.sample | 8 + .../jstips/hooks/pre-applypatch.sample | 14 + .../modules/jstips/hooks/pre-commit.sample | 49 + .../modules/jstips/hooks/pre-push.sample | 53 + .../modules/jstips/hooks/pre-rebase.sample | 169 +++ .../jstips/hooks/prepare-commit-msg.sample | 36 + .../modules/jstips/hooks/update.sample | 128 ++ .../git.git/modules/jstips/index | Bin 0 -> 427 bytes .../git.git/modules/jstips/info/exclude | 6 + .../git.git/modules/jstips/logs/HEAD | 1 + .../modules/jstips/logs/refs/heads/master | 1 + .../jstips/logs/refs/remotes/origin/HEAD | 1 + ...68a55e02b6b7b75582924204669e4f3ed5276f.idx | Bin 0 -> 6728 bytes ...8a55e02b6b7b75582924204669e4f3ed5276f.pack | Bin 0 -> 1081001 bytes .../git.git/modules/jstips/packed-refs | 2 + .../git.git/modules/jstips/refs/heads/master | 1 + .../modules/jstips/refs/remotes/origin/HEAD | 1 + .../3e/2fe2f8226faab789f70d6d8a7231e4f2bd54df | Bin 0 -> 165 bytes .../40/f4e79926a85134d4c905d04e70573b6616f3bc | Bin 0 -> 126 bytes .../50/b89367d8f0acd312ef5aa012eac20a75c91351 | Bin 0 -> 85 bytes .../54/3b9bebdc6bd5c4b22136034a95dd097a57d3dd | Bin 0 -> 51 bytes .../d2/b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 | Bin 0 -> 156 bytes .../d3/e073baf592c56614c68ead9e2cd0a3880140cd | 1 + .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 0 -> 15 bytes .../git.git/refs/heads/master | 1 + .../jstips/CONTRIBUTING.md | 20 + .../git/repo-with-submodules/jstips/README.md | 323 +++++ .../git/repo-with-submodules/jstips/git.git | 1 + .../jstips/resources/jstips-header-blog.gif | Bin 0 -> 1061017 bytes .../jstips/resources/log.js | 3 + 104 files changed, 9897 insertions(+) create mode 100644 spec/fixtures/git/repo-with-submodules/.gitmodules create mode 100644 spec/fixtures/git/repo-with-submodules/README create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/config create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/description create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/index create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/info/exclude create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/index create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/info/exclude create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/packed-refs create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/ORIG_HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/config create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/description create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/gitdir create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/applypatch-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/post-update.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-applypatch.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-commit.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-push.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-rebase.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/prepare-commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/update.sample create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/index create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/info/exclude create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/objects/pack/pack-e568a55e02b6b7b75582924204669e4f3ed5276f.idx create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/objects/pack/pack-e568a55e02b6b7b75582924204669e4f3ed5276f.pack create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/packed-refs create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/3e/2fe2f8226faab789f70d6d8a7231e4f2bd54df create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/40/f4e79926a85134d4c905d04e70573b6616f3bc create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/50/b89367d8f0acd312ef5aa012eac20a75c91351 create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/54/3b9bebdc6bd5c4b22136034a95dd097a57d3dd create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/d2/b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/d3/e073baf592c56614c68ead9e2cd0a3880140cd create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/CONTRIBUTING.md create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/README.md create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/git.git create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/resources/jstips-header-blog.gif create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/resources/log.js diff --git a/spec/fixtures/git/repo-with-submodules/.gitmodules b/spec/fixtures/git/repo-with-submodules/.gitmodules new file mode 100644 index 000000000..40f4e7992 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/.gitmodules @@ -0,0 +1,6 @@ +[submodule "jstips"] + path = jstips + url = https://github.com/loverajoel/jstips +[submodule "You-Dont-Need-jQuery"] + path = You-Dont-Need-jQuery + url = https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/README b/spec/fixtures/git/repo-with-submodules/README new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc new file mode 100644 index 000000000..e2472af86 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc @@ -0,0 +1,3 @@ +{ + presets: ["es2015", "stage-0"] +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc new file mode 100644 index 000000000..46faa7c5f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": "eslint-config-airbnb", + "env": { + "browser": true, + "mocha": true, + "node": true + }, + "rules": { + "valid-jsdoc": 2, + "no-param-reassign": 0, + "comma-dangle": 0, + "one-var": 0, + "no-else-return": 1, + "no-unused-expressions": 0, + "indent": 1, + "eol-last": 0 + } +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore new file mode 100644 index 000000000..539682cbf --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +*.log +node_modules +coverage +logs diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml new file mode 100644 index 000000000..69cf1679a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "5" + - "4" +before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +script: + - npm run lint + - npm test diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE new file mode 100644 index 000000000..df3175928 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 oneuijs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md new file mode 100644 index 000000000..652124255 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md @@ -0,0 +1,637 @@ + +> #### You Don't Need jQuery + +Tú no necesitas jQuery +--- +El desarrollo Frontend evoluciona día a día, y los navegadores modernos ya han implementado nativamente APIs para trabajar con DOM/BOM, las cuales son muy buenas, por lo que definitivamente no es necesario aprender jQuery desde cero para manipular el DOM. En la actualidad, gracias al surgimiento de librerías frontend como React, Angular y Vue, manipular el DOM es contrario a los patrones establecidos, y jQuery se ha vuelto menos importante. Este proyecto resume la mayoría de métodos alternativos a jQuery, pero de forma nativa con soporte IE 10+. + +## Tabla de Contenidos + +1. [Query Selector](#query-selector) +1. [CSS & Estilo](#css--estilo) +1. [Manipulación DOM](#manipulación-dom) +1. [Ajax](#ajax) +1. [Eventos](#eventos) +1. [Utilidades](#utilidades) +1. [Traducción](#traducción) +1. [Soporte de Navegadores](#soporte-de-navegadores) + + +## Query Selector + +En lugar de los selectores comunes como clase, id o atributos podemos usar `document.querySelector` o `document.querySelectorAll` como alternativas. Las diferencias radican en: +* `document.querySelector` devuelve el primer elemento que cumpla con la condición +* `document.querySelectorAll` devuelve todos los elementos que cumplen con la condición en forma de NodeList. Puede ser convertido a Array usando `[].slice.call(document.querySelectorAll(selector) || []);` +* Si ningún elemento cumple con la condición, jQuery retornaría `[]` mientras la API DOM retornaría `null`. Nótese el NullPointerException. Se puede usar `||` para establecer el valor por defecto al no encontrar elementos, como en `document.querySelectorAll(selector) || []` + +> Notice: `document.querySelector` and `document.querySelectorAll` are quite **SLOW**, try to use `getElementById`, `document.getElementsByClassName` o `document.getElementsByTagName` if you want to Obtener a performance bonus. + +- [1.0](#1.0) Buscar por selector + + ```js + // jQuery + $('selector'); + + // Nativo + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Buscar por Clase + + ```js + // jQuery + $('.class'); + + // Nativo + document.querySelectorAll('.class'); + + // Forma alternativa + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Buscar por id + + ```js + // jQuery + $('#id'); + + // Nativo + document.querySelector('#id'); + + // Forma alternativa + document.getElementById('id'); + ``` + +- [1.3](#1.3) Buscar por atributo + + ```js + // jQuery + $('a[target=_blank]'); + + // Nativo + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Buscar + + + Buscar nodos + + ```js + // jQuery + $el.find('li'); + + // Nativo + el.querySelectorAll('li'); + ``` + + + Buscar "body" + + ```js + // jQuery + $('body'); + + // Nativo + document.body; + ``` + + + Buscar Atributo + + ```js + // jQuery + $el.attr('foo'); + + // Nativo + e.getAttribute('foo'); + ``` + + + Buscar atributo "data" + + ```js + // jQuery + $el.data('foo'); + + // Nativo + // Usando getAttribute + el.getAttribute('data-foo'); + // También puedes utilizar `dataset` desde IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Elementos Hermanos/Previos/Siguientes + + + Elementos hermanos + + ```js + // jQuery + $el.siblings(); + + // Nativo + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Elementos previos + + ```js + // jQuery + $el.prev(); + + // Nativo + el.previousElementSibling; + ``` + + + Elementos siguientes + + ```js + // jQuery + $el.next(); + + // Nativo + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Retorna el elemento más cercano que coincida con la condición, partiendo desde el nodo actual hasta document. + + ```js + // jQuery + $el.closest(queryString); + + // Nativo + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Obtiene los ancestros de cada elemento en el set actual de elementos que cumplan con la condición, sin incluir el actual + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Nativo + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // Partir desde el elemento padre + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Formularios + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Nativo + document.querySelector('#my-input').value; + ``` + + + Obtener el índice de e.currentTarget en `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Nativo + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Contenidos de Iframe + + `$('iframe').contents()` devuelve `contentDocument` para este iframe específico + + + Contenidos de Iframe + + ```js + // jQuery + $iframe.contents(); + + // Nativo + iframe.contentDocument; + ``` + + + Buscar dentro de un Iframe + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Nativo + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## CSS & Estilo + +- [2.1](#2.1) CSS + + + Obtener Estilo + + ```js + // jQuery + $el.css("color"); + + // Nativo + // NOTA: Bug conocido, retornará 'auto' si el valor de estilo es 'auto' + const win = el.ownerDocument.defaultView; + // null significa que no tiene pseudo estilos + win.getComputedStyle(el, null).color; + ``` + + + Establecer style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Nativo + el.style.color = '#ff0011'; + ``` + + + Obtener/Establecer Estilos + + Nótese que si se desea establecer múltiples estilos a la vez, se puede utilizar el método [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) en el paquete oui-dom-utils. + + + Agregar clase + + ```js + // jQuery + $el.addClass(className); + + // Nativo + el.classList.add(className); + ``` + + + Quitar Clase + + ```js + // jQuery + $el.removeClass(className); + + // Nativo + el.classList.remove(className); + ``` + + + Consultar si tiene clase + + ```js + // jQuery + $el.hasClass(className); + + // Nativo + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Nativo + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Ancho y Alto son teóricamente idénticos. Usaremos el Alto como ejemplo: + + + Alto de Ventana + + ```js + // alto de ventana + $(window).height(); + // Sin scrollbar, se comporta como jQuery + window.document.documentElement.clientHeight; + // Con scrollbar + window.innerHeight; + ``` + + + Alto de Documento + + ```js + // jQuery + $(document).height(); + + // Nativo + document.documentElement.scrollHeight; + ``` + + + Alto de Elemento + + ```js + // jQuery + $el.height(); + + // Nativo + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // Precisión de integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // Precisión de decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Posición & Offset + + + Posición + + ```js + // jQuery + $el.position(); + + // Nativo + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Nativo + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Posición del Scroll Vertical + + ```js + // jQuery + $(window).scrollTop(); + + // Nativo + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Manipulación DOM + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Nativo + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Obtener Texto + + ```js + // jQuery + $el.text(); + + // Nativo + el.textContent; + ``` + + + Establecer Texto + + ```js + // jQuery + $el.text(string); + + // Nativo + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Obtener HTML + + ```js + // jQuery + $el.html(); + + // Nativo + el.innerHTML; + ``` + + + Establecer HTML + + ```js + // jQuery + $el.html(htmlString); + + // Nativo + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Añadir elemento hijo después del último hijo del elemento padre + + ```js + // jQuery + $el.append("
hello
"); + + // Nativo + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + Añadir elemento hijo después del último hijo del elemento padre + + ```js + // jQuery + $el.prepend("
hello
"); + + // Nativo + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Insertar un nuevo nodo antes del primero de los elementos seleccionados + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insertar un nuevo nodo después de los elementos seleccionados + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Ajax + +Reemplazar con [fetch](https://github.com/camsong/fetch-ie8) y [fetch-jsonp](https://github.com/camsong/fetch-jsonp) ++[Fetch API](https://fetch.spec.whatwg.org/) es el nuevo estándar quue reemplaza a XMLHttpRequest para efectuar peticiones AJAX. Funciona en Chrome y Firefox, como también es posible usar un polyfill en otros navegadores. ++ ++Es una buena alternativa utilizar [github/fetch](http://github.com/github/fetch) en IE9+ o [fetch-ie8](https://github.com/camsong/fetch-ie8/) en IE8+, [fetch-jsonp](https://github.com/camsong/fetch-jsonp) para efectuar peticiones JSONP. +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Eventos + +Para un reemplazo completo con namespace y delegación, utilizar https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Asignar un evento con "on" + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Nativo + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Desasignar un evento con "off" + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Nativo + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Nativo + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Utilidades + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Nativo + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Nativo + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Utilizar polyfill para object.assign https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Nativo + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Nativo + el !== child && el.contains(child); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Traducción + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Soporte de Navegadores + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Última ✔ | Última ✔ | 10+ ✔ | Última ✔ | 6.1+ ✔ | + +# Licencia + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md new file mode 100644 index 000000000..13ca75b6c --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md @@ -0,0 +1,634 @@ +## Anda tidak memerlukan jQuery + +Dewasa ini perkembangan environment frontend sangatlah pesat, dimana banyak browser sudah mengimplementasikan DOM/BOM APIs dengan baik. Kita tidak perlu lagi belajar jQuery dari nol untuk keperluan manipulasi DOM atau events. Disaat yang sama; dengan berterimakasih kepada library frontend terkini seperti React, Angular dan Vue; Memanipulasi DOM secara langsung telah menjadi anti-pattern alias sesuatu yang tidak perlu dilakukan. Dengan kata lain, jQuery sekarang menjadi semakin tidak diperlukan. Projek ini memberikan informasi mengenai metode alternatif dari jQuery untuk implementasi Native dengan support untuk browser IE 10+. + + +## Daftar Isi + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css-style) +1. [DOM Manipulation](#dom-manipulation) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utilities](#utilities) +1. [Translation](#translation) +1. [Browser Support](#browser-yang-di-support) + +## Query Selector + +Untuk selector-selector umum seperti class, id atau attribute, kita dapat menggunakan `document.querySelector` atau `document.querySelectorAll` sebagai pengganti. Perbedaan diantaranya adalah: +* `document.querySelector` mengembalikan elemen pertama yang cocok +* `document.querySelectorAll` mengembalikan semua elemen yang cocok sebagai NodeList. Hasilnya bisa dikonversikan menjadi Array `[].slice.call(document.querySelectorAll(selector) || []);` +* Bila tidak ada hasil pengembalian elemen yang cocok, jQuery akan mengembalikan `[]` sedangkan DOM API akan mengembalikan `null`. Mohon diperhatikan mengenai Null Pointer Exception. Anda juga bisa menggunakan operator `||` untuk set nilai awal jika hasil pencarian tidak ditemukan : `document.querySelectorAll(selector) || []` + +> Perhatian: `document.querySelector` dan `document.querySelectorAll` sedikit **LAMBAT**. Silahkan menggunakan `getElementById`, `document.getElementsByClassName` atau `document.getElementsByTagName` jika anda menginginkan tambahan performa. + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query by class + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query by id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query menggunakan attribute + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Pencarian. + + + Mencari nodes + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Mencari body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + Mencari Attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Mencari data attribute + + ```js + // jQuery + $el.data('foo'); + + // Native + // gunakan getAttribute + el.getAttribute('data-foo'); + // anda juga bisa menggunakan `dataset` bila anda perlu support IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Elemen-elemen Sibling/Previous/Next + + + Elemen Sibling + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Elemen Previous + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Elemen Next + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Mengembalikan elemen pertama yang cocok dari selector yang digunakan, dengan cara mencari mulai dari elemen-sekarang sampai ke document. + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Digunakan untuk mendapatkan "ancestor" dari setiap elemen yang ditemukan. Namun tidak termasuk elemen-sekarang yang didapat dari pencarian oleh selector, DOM node, atau object jQuery. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` mengembalikan `contentDocument` + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ back to top](#daftar-isi)** + +## CSS Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // PERHATIAN: ada bug disini, dimana fungsi ini akan mengembalikan nilai 'auto' bila nilai dari atribut style adalah 'auto' + const win = el.ownerDocument.defaultView; + // null artinya tidak mengembalikan pseudo styles + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Mohon dicatat jika anda ingin men-set banyak style bersamaan, anda dapat menemukan referensi di metode [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) pada package oui-dom-utils + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Secara teori, width dan height identik, contohnya Height: + + + Window height + + ```js + // window height + $(window).height(); + // without scrollbar, behaves like jQuery + window.document.documentElement.clientHeight; + // with scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#daftar-isi)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Menambahkan elemen-anak setelah anak terakhir dari elemen-parent + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Memasukkan node baru sebelum elemen yang dipilih. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Memasukkan node baru sesudah elemen yang dipilih. + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ back to top](#daftar-isi)** + +## Ajax + +Gantikan dengan [fetch](https://github.com/camsong/fetch-ie8) dan [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ back to top](#daftar-isi)** + +## Events + +Untuk penggantian secara menyeluruh dengan namespace dan delegation, rujuk ke https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind event dengan menggunakan on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind event dengan menggunakan off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#daftar-isi)** + +## Utilities + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Extend, use object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ back to top](#daftar-isi)** + +## Terjemahan + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Browser yang di Support + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md new file mode 100644 index 000000000..2af89202a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md @@ -0,0 +1,651 @@ +## Non hai bisogno di jQuery + +Il mondo del Frontend si evolve rapidamente oggigiorno, i browsers moderni hanno gia' implementato un'ampia gamma di DOM/BOM API soddisfacenti. Non dobbiamo imparare jQuery dalle fondamenta per la manipolazione del DOM o di eventi. Nel frattempo, grazie al prevalicare di librerie per il frontend come React, Angular a Vue, manipolare il DOM direttamente diventa un anti-pattern, di consequenza jQuery non e' mai stato meno importante. Questo progetto sommarizza la maggior parte dei metodi e implementazioni alternative a jQuery, con il supporto di IE 10+. + +## Tabella contenuti + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [Manipolazione DOM](#manipolazione-dom) +1. [Ajax](#ajax) +1. [Eventi](#eventi) +1. [Utilities](#utilities) +1. [Alternative](#alternative) +1. [Traduzioni](#traduzioni) +1. [Supporto Browsers](#supporto-browsers) + +## Query Selector + +Al posto di comuni selettori come class, id o attributi possiamo usare `document.querySelector` o `document.querySelectorAll` per sostituzioni. La differenza risiede in: +* `document.querySelector` restituisce il primo elemento combiaciante +* `document.querySelectorAll` restituisce tutti gli elementi combiacianti della NodeList. Puo' essere convertito in Array usando `[].slice.call(document.querySelectorAll(selector) || []);` +* Se nessun elemento combiacia, jQuery restituitirebbe `[]` li' dove il DOM API ritornera' `null`. Prestate attenzione al Null Pointer Exception. Potete anche usare `||` per settare valori di default se non trovato, come `document.querySelectorAll(selector) || []` + +> Notare: `document.querySelector` e `document.querySelectorAll` sono abbastanza **SLOW**, provate ad usare `getElementById`, `document.getElementsByClassName` o `document.getElementsByTagName` se volete avere un bonus in termini di performance. + +- [1.0](#1.0) Query da selettore + + ```js + // jQuery + $('selector'); + + // Nativo + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query da classe + + ```js + // jQuery + $('.class'); + + // Nativo + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query da id + + ```js + // jQuery + $('#id'); + + // Nativo + document.querySelector('#id'); + + // o + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query da attributo + + ```js + // jQuery + $('a[target=_blank]'); + + // Nativo + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Trovare qualcosa. + + + Trovare nodes + + ```js + // jQuery + $el.find('li'); + + // Nativo + el.querySelectorAll('li'); + ``` + + + Trovare body + + ```js + // jQuery + $('body'); + + // Nativo + document.body; + ``` + + + Trovare Attributi + + ```js + // jQuery + $el.attr('foo'); + + // Nativo + e.getAttribute('foo'); + ``` + + + Trovare attributo data + + ```js + // jQuery + $el.data('foo'); + + // Nativo + // using getAttribute + el.getAttribute('data-foo'); + // potete usare `dataset` solo se supportate IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Fratelli/Precedento/Successivo Elemento + + + Elementi fratelli + + ```js + // jQuery + $el.siblings(); + + // Nativo + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Elementi precedenti + + ```js + // jQuery + $el.prev(); + + // Nativo + el.previousElementSibling; + ``` + + + Elementi successivi + + ```js + // jQuery + $el.next(); + + // Nativo + el.nextElementSibling; + ``` + +- [1.6](#1.6) Il piu' vicino + + Restituisce il primo elementi combiaciante il selettore fornito, attraversando dall'elemento corrente fino al document . + + ```js + // jQuery + $el.closest(queryString); + + // Nativo - Solo ultimo, NO IE + el.closest(selector); + + // Nativo - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Fino a parenti + + Ottiene il parente di ogni elemento nel set corrente di elementi combiacianti, fino a ma non incluso, l'elemento combiaciante il selettorer, DOM node, o jQuery object. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Nativo + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // il match parte dal parente + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Nativo + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` restituisce `contentDocument` per questo specifico iframe + + + Iframe contenuti + + ```js + // jQuery + $iframe.contents(); + + // Nativo + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Nativo + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ back to top](#table-of-contents)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Ottenere style + + ```js + // jQuery + $el.css("color"); + + // Nativo + // NOTA: Bug conosciuto, restituira' 'auto' se il valore di style e' 'auto' + const win = el.ownerDocument.defaultView; + // null significa che non restituira' lo psuedo style + win.getComputedStyle(el, null).color; + ``` + + + Settare style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Nativo + el.style.color = '#ff0011'; + ``` + + + Ottenere/Settare Styles + + Nota che se volete settare styles multipli in una sola volta, potete riferire [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) metodo in oui-dom-utils package. + + + + Aggiungere classe + + ```js + // jQuery + $el.addClass(className); + + // Nativo + el.classList.add(className); + ``` + + + Rimouvere class + + ```js + // jQuery + $el.removeClass(className); + + // Nativo + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Nativo + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Nativo + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width e Height sono teoricamente identici, prendendo Height come esempio: + + + Window height + + ```js + // window height + $(window).height(); + // senza scrollbar, si comporta comporta jQuery + window.document.documentElement.clientHeight; + // con scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Nativo + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Nativo + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // preciso a intero(quando `border-box`, e' `height`; quando `content-box`, e' `height + padding + border`) + el.clientHeight; + // preciso a decimale(quando `border-box`, e' `height`; quando `content-box`, e' `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Nativo + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Nativo + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Nativo + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#table-of-contents)** + +## Manipolazione DOM + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Nativo + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Nativo + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Nativo + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Ottenere HTML + + ```js + // jQuery + $el.html(); + + // Nativo + el.innerHTML; + ``` + + + Settare HTML + + ```js + // jQuery + $el.html(htmlString); + + // Nativo + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + appendere elemento figlio dopo l'ultimo elemento figlio del genitore + + ```js + // jQuery + $el.append("
hello
"); + + // Nativo + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Nativo + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Inserire un nuovo node dopo l'elmento selezionato + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insert a new node after the selected elements + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) is + + Restituisce `true` se combacia con l'elemento selezionato + + ```js + // jQuery - Notare `is` funziona anche con `function` o `elements` non di importanza qui + $el.is(selector); + + // Nativo + el.matches(selector); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Ajax + +Sostituire con [fetch](https://github.com/camsong/fetch-ie8) and [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ back to top](#table-of-contents)** + +## Eventi + +Per una completa sostituzione con namespace e delegation, riferire a https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind un evento con on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Nativo + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Nativo + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Nativo + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Utilities + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Nativo + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Nativo + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Extend, usa object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Nativo + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Nativo + el !== child && el.contains(child); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Alternative + +* [Forse non hai bisogno di jQuery](http://youmightnotneedjquery.com/) - Esempi di come creare eventi comuni, elementi, ajax etc usando puramente javascript. +* [npm-dom](http://github.com/npm-dom) e [webmodules](http://github.com/webmodules) - Organizzazione dove puoi trovare moduli per il DOM individuale su NPM + +## Traduzioni + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Italiano](./README-it.md) +* [Türkçe](./README-tr.md) + +## Supporto Browsers + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Ultimo ✔ | Ultimo ✔ | 10+ ✔ | Ultimo ✔ | 6.1+ ✔ | + +# Licenza + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md new file mode 100644 index 000000000..1f0a759c3 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md @@ -0,0 +1,615 @@ +## Anda tidak memerlukan jQuery + +Mutakhir ini perkembangan dalam persekitaran frontend berlaku begitu pesat sekali. Justeru itu kebanyakan pelayar moden telahpun menyediakan API yang memadai untuk pengaksesan DOM/BOM. Kita tak payah lagi belajar jQuery dari asas untuk memanipulasi DOM dan acara-acara. Projek ini menawarkan perlaksanaan alternatif kepada kebanyakan kaedah-kaedah jQuery yang menyokong IE 10+. + +## Isi Kandungan + +1. [Pemilihan elemen](#pemilihan-elemen) +1. [CSS & Penggayaan](#css-penggayaan) +1. [Manipulasi DOM](#manipulasi-dom) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utiliti](#utiliti) +1. [Terjemahan](#terjemahan) +1. [Browser Support](#browser-support) + +## Pemilihan Elemen + +Pemilihan elemen yang umum seperti class, id atau atribut, biasanya kita boleh pakai `document.querySelector` atau `document.querySelectorAll` sebagai ganti. Bezanya terletak pada +* `document.querySelector` akan mengembalikan elemen pertama sekali yang sepadan dijumpai +* `document.querySelectorAll` akan mengembalikan kesemua elemen yang sepadan dijumpai kedalam sebuah NodeList. Ia boleh ditukar kedalam bentuk array menggunakan `[].slice.call` +* Sekiranya tiada elemen yang sepadan dijumpai, jQuery akan mengembalikan `[]` dimana API DOM pula akan mengembalikan `null`. Sila ambil perhatian pada Null Pointer Exception + +> AWAS: `document.querySelector` dan `document.querySelectorAll` agak **LEMBAB** berbanding `getElementById`, `document.getElementsByClassName` atau `document.getElementsByTagName` jika anda menginginkan bonus dari segi prestasi. + +- [1.1](#1.1) Pemilihan menggunakan class + + ```js + // jQuery + $('.css'); + + // Native + document.querySelectorAll('.css'); + ``` + +- [1.2](#1.2) Pemilihan menggunakan id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + ``` + +- [1.3](#1.3) Pemilihan menggunakan atribut + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Cari sth. + + + Find nodes + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Cari body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + Cari Attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Cari atribut data + + ```js + // jQuery + $el.data('foo'); + + // Native + // menggunakan getAttribute + el.getAttribute('data-foo'); + // anda boleh juga gunakan `dataset` jika ingin pakai IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Next elements + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Return the first matched element by provided selector, traversing from current element to document. + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` returns `contentDocument` for this specific iframe + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ back to top](#table-of-contents)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: Known bug, will return 'auto' if style value is 'auto' + const win = el.ownerDocument.defaultView; + // null means not return presudo styles + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Note that if you want to set multiple styles once, you could refer to [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) method in oui-dom-utils package. + + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width and Height are theoretically identical, take Height as example: + + + Window height + + ```js + // window height + $(window).height(); + // without scrollbar, behaves like jQuery + window.document.documentElement.clientHeight; + // with scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#table-of-contents)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + append child element after the last child of parent element + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Insert a new node before the selected elements + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insert a new node after the selected elements + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Ajax + +Replace with [fetch](https://github.com/camsong/fetch-ie8) and [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ back to top](#table-of-contents)** + +## Events + +For a complete replacement with namespace and delegation, refer to https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind an event with on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Utility + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + String.trim(string); + ``` + +- [6.3](#6.3) Object Assign + + Extend, use object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Terjemahan + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [English](./README.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Sokongan Pelayar + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Lesen + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md new file mode 100644 index 000000000..561bdb96f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md @@ -0,0 +1,650 @@ +## Вам не нужен jQuery + +В наше время среда фронт энд разработки быстро развивается, современные браузеры уже реализовали значимую часть DOM/BOM APIs и это хорошо. Вам не нужно изучать jQuery с нуля для манипуляцией DOM'ом или обектами событий. В то же время, благодаря лидирующим фронт энд библиотекам, таким как React, Angular и Vue, манипуляция DOM'ом напрямую становится противо шаблонной, jQuery никогда не был менее важен. Этот проект суммирует большинство альтернатив методов jQuery в нативном исполнении с поддержкой IE 10+. + +## Содержание + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [Манипуляция DOM](#Манипуляции-dom) +1. [Ajax](#ajax) +1. [События](#События) +1. [Утилиты](#Утилиты) +1. [Альтернативы](#Альтернативы) +1. [Переводы](#Переводы) +1. [Поддержка браузеров](#Поддержка-браузеров) + +## Query Selector +Для часто используемых селекторов, таких как class, id или attribute мы можем использовать `document.querySelector` или `document.querySelectorAll` для замены. Разница такова: +* `document.querySelector` возвращает первый совпавший элемент +* `document.querySelectorAll` возвращает все совспавшие элементы как коллекцию узлов(NodeList). Его можно конвертировать в массив используя `[].slice.call(document.querySelectorAll(selector) || []);` +* Если никакие элементы не совпадут, jQuery вернет `[]` где DOM API вернет `null`. Обратите внимание на указатель исключения Null (Null Pointer Exception). Вы так же можете использовать `||` для установки значения по умолчанию если не было найдемо совпадений `document.querySelectorAll(selector) || []` + +> Заметка: `document.querySelector` и `document.querySelectorAll` достаточно **МЕДЛЕННЫ**, старайтесь использовать `getElementById`, `document.getElementsByClassName` или `document.getElementsByTagName` если хотите улучшить производительность. + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Нативно + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Запрос по классу + + ```js + // jQuery + $('.class'); + + // Нативно + document.querySelectorAll('.class'); + + // или + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Запрос по ID + + ```js + // jQuery + $('#id'); + + // Нативно + document.querySelector('#id'); + + // или + document.getElementById('id'); + ``` + +- [1.3](#1.3) Запрос по атрибуту + + ```js + // jQuery + $('a[target=_blank]'); + + // Нативно + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Найти среди потомков + + + Найти nodes + + ```js + // jQuery + $el.find('li'); + + // Нативно + el.querySelectorAll('li'); + ``` + + + Найти body + + ```js + // jQuery + $('body'); + + // Нативно + document.body; + ``` + + + Найти атрибуты + + ```js + // jQuery + $el.attr('foo'); + + // Нативно + e.getAttribute('foo'); + ``` + + + Найти data attribute + + ```js + // jQuery + $el.data('foo'); + + // Нативно + // используя getAttribute + el.getAttribute('data-foo'); + // также можно использовать `dataset`, если не требуется поддержка ниже IE 11. + el.dataset['foo']; + ``` + +- [1.5](#1.5) Родственные/Предыдущие/Следующие Элементы + + + Родственные элементы + + ```js + // jQuery + $el.siblings(); + + // Нативно + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Предыдущие элементы + + ```js + // jQuery + $el.prev(); + + // Нативно + el.previousElementSibling; + ``` + + + Следующие элементы + + ```js + // jQuery + $el.next(); + + // Нативно + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Возвращает первый совпавший элемент по предоставленному селектору, обоходя от текущего элементы до документа. + + ```js + // jQuery + $el.closest(queryString); + + // Нативно - Only latest, NO IE + el.closest(selector); + + // Нативно - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Родители до + + Получить родителей кажого элемента в текущем сете совпавших элементов, но не включая элемент совпавший с селектором, узел DOM'а, или объект jQuery. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Нативно + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // Совпадать начиная от родителя + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) От + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Нативно + document.querySelector('#my-input').value; + ``` + + + получить индекс e.currentTarget между `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Нативно + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Контент Iframe + + `$('iframe').contents()` возвращает `contentDocument` для именно этого iframe + + + Контент Iframe + + ```js + // jQuery + $iframe.contents(); + + // Нативно + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Нативно + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ Наверх](#Содержание)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Получить стиль + + ```js + // jQuery + $el.css("color"); + + // Нативно + // ЗАМЕТКА: Известная ошика, возвращает 'auto' если значение стиля 'auto' + const win = el.ownerDocument.defaultView; + // null означает не возвращать псевдостили + win.getComputedStyle(el, null).color; + ``` + + + Присвоение style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Нативно + el.style.color = '#ff0011'; + ``` + + + Получение/Присвоение стилей + + Заметьте что если вы хотите присвоить несколько стилей за раз, вы можете сослаться на [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) метод в oui-dom-utils package. + + + + Добавить класс + + ```js + // jQuery + $el.addClass(className); + + // Нативно + el.classList.add(className); + ``` + + + Удалить class + + ```js + // jQuery + $el.removeClass(className); + + // Нативно + el.classList.remove(className); + ``` + + + Имеет класс + + ```js + // jQuery + $el.hasClass(className); + + // Нативно + el.classList.contains(className); + ``` + + + Переключать класс + + ```js + // jQuery + $el.toggleClass(className); + + // Нативно + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Ширина и Высота + + Ширина и высота теоритечески идентичны, например возьмем высоту: + + + высота окна + + ```js + // Высота окна + $(window).height(); + // без скроллбара, ведет себя как jQuery + window.document.documentElement.clientHeight; + // вместе с скроллбаром + window.innerHeight; + ``` + + + высота документа + + ```js + // jQuery + $(document).height(); + + // Нативно + document.documentElement.scrollHeight; + ``` + + + Высота элемента + + ```js + // jQuery + $el.height(); + + // Нативно + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // С точностью до целого числа(когда `border-box`, это `height`; когда `content-box`, это `height + padding + border`) + el.clientHeight; + // с точностью до десятых(когда `border-box`, это `height`; когда `content-box`, это `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Позиция и смещение + + + Позиция + + ```js + // jQuery + $el.position(); + + // Нативно + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Смещение + + ```js + // jQuery + $el.offset(); + + // Нативно + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Прокрутка вверх + + ```js + // jQuery + $(window).scrollTop(); + + // Нативно + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ Наверх](#Содержание)** + +## Манипуляции DOM + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Нативно + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Текст + + + Получить текст + + ```js + // jQuery + $el.text(); + + // Нативно + el.textContent; + ``` + + + Присвоить текст + + ```js + // jQuery + $el.text(string); + + // Нативно + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Получить HTML + + ```js + // jQuery + $el.html(); + + // Нативно + el.innerHTML; + ``` + + + Присвоить HTML + + ```js + // jQuery + $el.html(htmlString); + + // Нативно + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Добавление элемента ребенка после последнего ребенка элемента родителя + + ```js + // jQuery + $el.append("
hello
"); + + // Нативно + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Нативно + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Вставка нового элемента перед выбранным элементом + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Нативно + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Вставка новго элемента после выбранного элемента + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Нативно + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) is + + Возвращает `true` если совпадает с селектором запроса + + ```js + // jQuery - заметьте что `is` так же работает с `function` или `elements` которые не имют к этому отношения + $el.is(selector); + + // Нативно + el.matches(selector); + ``` + +**[⬆ Наверх](#Содержание)** + +## Ajax + +Заменить с [fetch](https://github.com/camsong/fetch-ie8) и [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ Наверх](#Содержание)** + +## События + +Для полной замены с пространством имен и делегация, сослаться на [oui-dom-events](https://github.com/oneuijs/oui-dom-events) + +- [5.1](#5.1) Связать событие используя on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Нативно + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Отвязать событие используя off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Нативно + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Нативно + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ Наверх](#Содержание)** + +## Утилиты + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Нативно + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Нативно + string.trim(); + ``` + +- [6.3](#6.3) Назначение объекта + + Дополнительно, используйте полифил object.assign https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Нативно + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Нативно + el !== child && el.contains(child); + ``` + +**[⬆ Наверх](#Содержание)** + +## Альтернативы + +* [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - Примеры как исполняются частые события, элементы, ajax и тд с ванильным javascript. +* [npm-dom](http://github.com/npm-dom) и [webmodules](http://github.com/webmodules) - Отдельные DOM модули можно найти на NPM + +## Переводы + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Поддержка браузеров + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md new file mode 100644 index 000000000..85d614f5f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md @@ -0,0 +1,665 @@ +## jQuery'e İhtiyacınız Yok [![Build Status](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery.svg)](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery) + +Önyüz ortamları bugünlerde çok hızlı gelişiyor, öyle ki modern tarayıcılar DOM/DOM APİ'lere ait önemli gereklilikleri çoktan yerine getirdiler. DOM işleme ve olaylar için, en baştan jQuery ögrenmemize gerek kalmadı. Bu arada, üstünlükleri ile jQuery'i önemsizleştiren ve doğrudan DOM değişikliklerinin bir Anti-pattern olduğunu gösteren, React, Angular ve Vue gibi gelişmiş önyüz kütüphanelerine ayrıca teşekkür ederiz. Bu proje, IE10+ desteği ile coğunluğu jQuery yöntemlerine alternatif olan yerleşik uygulamaları içerir. + +## İçerik Tablosu + +1. [Sorgu seçiciler](#sorgu-seçiciler) +1. [CSS & Stil](#css--stil) +1. [DOM düzenleme](#dom-düzenleme) +1. [Ajax](#ajax) +1. [Olaylar](#olaylar) +1. [Araçlar](#araçlar) +1. [Alternatifler](#alternatifler) +1. [Çeviriler](#Çeviriler) +1. [Tarayıcı desteği](#tarayıcı-desteği) + +## Sorgu seçiciler + +Yaygın olan class, id ve özellik seçiciler yerine, `document.querySelector` yada `document.querySelectorAll` kullanabiliriz. Ayrıldıkları nokta: +* `document.querySelector` ilk seçilen öğeyi döndürür +* `document.querySelectorAll` Seçilen tüm öğeleri NodeList olarak geri döndürür. `[].slice.call(document.querySelectorAll(selector) || []);` kullanarak bir diziye dönüştürebilirsiniz. +* Herhangi bir öğenin seçilememesi durumda ise, jQuery `[]` döndürürken, DOM API `null` döndürecektir. Null Pointer istisnası almamak için `||` ile varsayılan değere atama yapabilirsiniz, örnek: `document.querySelectorAll(selector) || []` + +> Uyarı: `document.querySelector` ve `document.querySelectorAll` biraz **YAVAŞ** olabilir, Daha hızlısını isterseniz, `getElementById`, `document.getElementsByClassName` yada `document.getElementsByTagName` kullanabilirsiniz. + +- [1.0](#1.0) Seçici ile sorgu + + ```js + // jQuery + $('selector'); + + // Yerleşik + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Sınıf ile sorgu + + ```js + // jQuery + $('.class'); + + // Yerleşik + document.querySelectorAll('.class'); + + // yada + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Id ile sorgu + + ```js + // jQuery + $('#id'); + + // Yerleşik + document.querySelector('#id'); + + // yada + document.getElementById('id'); + ``` + +- [1.3](#1.3) Özellik ile sorgu + + ```js + // jQuery + $('a[target=_blank]'); + + // Yerleşik + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Öğe erişimi + + + Node'a erişim + + ```js + // jQuery + $el.find('li'); + + // Yerleşik + el.querySelectorAll('li'); + ``` + + + Body'e erişim + + ```js + // jQuery + $('body'); + + // Yerleşik + document.body; + ``` + + + Özelliğe erişim + + ```js + // jQuery + $el.attr('foo'); + + // Yerleşik + el.getAttribute('foo'); + ``` + + + Data özelliğine erişim + + ```js + // jQuery + $el.data('foo'); + + // Yerleşik + // getAttribute kullanarak + el.getAttribute('data-foo'); + // Eğer IE 11+ kullanıyor iseniz, `dataset` ile de erişebilirsiniz + el.dataset['foo']; + ``` + +- [1.5](#1.5) Kardeş/Önceki/Sonraki öğeler + + + Kardeş öğeler + + ```js + // jQuery + $el.siblings(); + + // Yerleşik + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Önceki öğeler + + ```js + // jQuery + $el.prev(); + + // Yerleşik + el.previousElementSibling; + ``` + + + Sonraki öğeler + + ```js + // jQuery + $el.next(); + + // Yerleşik + el.nextElementSibling; + ``` + +- [1.6](#1.6) En yakın + + Verilen seçici ile eşleşen ilk öğeyi döndürür, geçerli öğeden başlayarak document'a kadar geçiş yapar. + + ```js + // jQuery + $el.closest(selector); + + // Yerleşik - Sadece en güncellerde, IE desteklemiyor + el.closest(selector); + + // Yerleşik - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Önceki atalar + + Verilen seçici ile eşleşen öğe veya DOM node veya jQuery nesnesi hariç, mevcut öğe ile aradaki tüm önceki ataları bir set dahilinde verir. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Yerleşik + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // eşleştirme, atadan başlar + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Yerleşik + document.querySelector('#my-input').value; + ``` + + + e.currentTarget ile `.radio` arasındaki dizini verir + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Yerleşik + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe İçeriği + + Mevcut Iframe için `$('iframe').contents()` yerine `contentDocument` döndürür. + + + Iframe İçeriği + + ```js + // jQuery + $iframe.contents(); + + // Yerleşik + iframe.contentDocument; + ``` + + + Iframe seçici + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Yerleşik + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## CSS & Stil + +- [2.1](#2.1) CSS + + + Stili verir + + ```js + // jQuery + $el.css("color"); + + // Yerleşik + // NOT: Bilinen bir hata, eğer stil değeri 'auto' ise 'auto' döndürür + const win = el.ownerDocument.defaultView; + // null sahte tipleri döndürmemesi için + win.getComputedStyle(el, null).color; + ``` + + + Stil değiştir + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Yerleşik + el.style.color = '#ff0011'; + ``` + + + Stil değeri al/değiştir + + Eğer aynı anda birden fazla stili değiştirmek istiyor iseniz, oui-dom-utils paketi içindeki [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) metoduna göz atınız. + + + + Sınıf ekle + + ```js + // jQuery + $el.addClass(className); + + // Yerleşik + el.classList.add(className); + ``` + + + Sınıf çıkart + + ```js + // jQuery + $el.removeClass(className); + + // Yerleşik + el.classList.remove(className); + ``` + + + sınfı var mı? + + ```js + // jQuery + $el.hasClass(className); + + // Yerleşik + el.classList.contains(className); + ``` + + + Sınfı takas et + + ```js + // jQuery + $el.toggleClass(className); + + // Yerleşik + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Genişlik ve Yükseklik + + Genişlik ve Yükseklik teorik olarak aynı şekilde, örnek olarak Yükseklik veriliyor + + + Window Yüksekliği + + ```js + // window yüksekliği + $(window).height(); + // kaydırma çubuğu olmaksızın, jQuery ile aynı + window.document.documentElement.clientHeight; + // kaydırma çubuğu ile birlikte + window.innerHeight; + ``` + + + Document yüksekliği + + ```js + // jQuery + $(document).height(); + + // Yerleşik + document.documentElement.scrollHeight; + ``` + + + Öğe yüksekliği + + ```js + // jQuery + $el.height(); + + // Yerleşik + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // Tamsayı olarak daha doğru olanı(`border-box` iken, `height` esas; `content-box` ise, `height + padding + border` esas alınır) + el.clientHeight; + // Ondalık olarak daha doğru olanı(`border-box` iken, `height` esas; `content-box` ise, `height + padding + border` esas alınır) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Pozisyon ve Ara-Açıklığı + + + Pozisyon + + ```js + // jQuery + $el.position(); + + // Yerleşik + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Ara-Açıklığı + + ```js + // jQuery + $el.offset(); + + // Yerleşik + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Üste kaydır + + ```js + // jQuery + $(window).scrollTop(); + + // Yerleşik + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## DOM düzenleme + +- [3.1](#3.1) Çıkartma + ```js + // jQuery + $el.remove(); + + // Yerleşik + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Metin + + + Get text + + ```js + // jQuery + $el.text(); + + // Yerleşik + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Yerleşik + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + HTML'i alma + + ```js + // jQuery + $el.html(); + + // Yerleşik + el.innerHTML; + ``` + + + HTML atama + + ```js + // jQuery + $el.html(htmlString); + + // Yerleşik + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Sona ekleme + + Ata öğenin son çocuğundan sonra öğe ekleme + + ```js + // jQuery + $el.append("
hello
"); + + // Yerleşik + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Öne ekleme + + ```js + // jQuery + $el.prepend("
hello
"); + + // Yerleşik + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) Öncesine Ekleme + + Seçili öğeden önceki yere yeni öğe ekleme + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Yerleşik + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) Sonrasına ekleme + + Seçili öğeden sonraki yere yeni öğe ekleme + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Yerleşik + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) eşit mi? + + Sorgu seçici ile eşleşiyor ise `true` döner + + ```js + // jQuery için not: `is` aynı zamanda `function` veya `elements` için de geçerlidir fakat burada bir önemi bulunmuyor + $el.is(selector); + + // Yerleşik + el.matches(selector); + ``` +- [3.9](#3.9) Klonlama + + Mevcut öğenin bir derin kopyasını oluşturur + + ```js + // jQuery + $el.clone(); + + // Yerleşik + el.cloneNode(); + + // Derin kopya için, `true` parametresi kullanınız + ``` +**[⬆ üste dön](#İçerik-tablosu)** + +## Ajax + +[Fetch API](https://fetch.spec.whatwg.org/) ajax için XMLHttpRequest yerine kullanan yeni standarttır. Chrome ve Firefox destekler, eski tarayıcılar için polyfill kullanabilirsiniz. + +IE9+ ve üstü için [github/fetch](http://github.com/github/fetch) yada IE8+ ve üstü için [fetch-ie8](https://github.com/camsong/fetch-ie8/), JSONP istekler için [fetch-jsonp](https://github.com/camsong/fetch-jsonp) deneyiniz. + +**[⬆ üste dön](#İçerik-tablosu)** + +## Olaylar + +Namespace ve Delegasyon ile tam olarak değiştirmek için, https://github.com/oneuijs/oui-dom-events sayfasına bakınız + +- [5.1](#5.1) on ile bir öğeye bağlama + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Yerleşik + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) off ile bir bağlamayı sonlandırma + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Yerleşik + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Tetikleyici + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Yerleşik + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## Araçlar + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Yerleşik + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Yerleşik + string.trim(); + ``` + +- [6.3](#6.3) Nesne atama + + Türetmek için, object.assign polyfill'ini deneyiniz https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Yerleşik + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) İçerme + + ```js + // jQuery + $.contains(el, child); + + // Yerleşik + el !== child && el.contains(child); + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## Alternatifler + +* [jQuery'e İhtiyacınız Yok](http://youmightnotneedjquery.com/) - Yaygın olan olay, öğe ve ajax işlemlerinin yalın Javascript'teki karşılıklarına ait örnekler +* [npm-dom](http://github.com/npm-dom) ve [webmodules](http://github.com/webmodules) - NPM için ayrı DOM modül organizasyonları + +## Çeviriler + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Tarayıcı Desteği + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Lisans + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md new file mode 100644 index 000000000..7eb60f456 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md @@ -0,0 +1,634 @@ +## Bạn không cần jQuery nữa đâu + +Ngày nay, môi trường lập trình front-end phát triển rất nhanh chóng, các trình duyệt hiện đại đã cung cấp các API đủ tốt để làm việc với DOM/BOM. Bạn không còn cần phải học về jQuery nữa. Đồng thời, nhờ sự ra đời của các thư viện như React, Angular và Vue đã khiến cho việc can thiệp trực tiếp vào DOM trở thành một việc không tốt. jQuery đã không còn quan trọng như trước nữa. Bài viết này tổng hợp những cách để thay thế các hàm của jQuery bằng các hàm được hỗ trợ bởi trình duyệt, và hó cũng hoạt động trên IE 10+ + +## Danh mục + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [Thao tác với DOM](#thao-tác-với-dom) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Hàm tiện ích](#hàm-tiện-ích) +1. [Ngôn ngữ khác](#ngôn-ngữ-khác) +1. [Các trình duyệt hỗ trợ](#các-trình-duyệt-hỗ-trợ) + +## Query Selector + +Đối với những selector phổ biến như class, id hoặc thuộc tính thì chúng ta có thể sử dụng `document.querySelector` hoặc `document.querySelectorAll` để thay thế cho jQuery selector. Sự khác biệt của hai hàm này là ở chỗ: + +* `document.querySelector` trả về element đầu tiên được tìm thấy +* `document.querySelectorAll` trả về tất cả các element được tìm thấy dưới dạng một instance của NodeList. Nó có thể được convert qua array bằng cách `[].slice.call(document.querySelectorAll(selector) || []);` +* Nếu không có element nào được tìm thấy, thì jQuery sẽ trả về một array rỗng `[]` trong khi đó DOM API sẽ trả về `null`. Hãy chú ý đến Null Pointer Exception. Bạn có thể sử dụng toán tử `||` để đặt giá trị default nếu như không có element nào được tìm thấy, ví dụ như `document.querySelectorAll(selector) || []` + +> Chú ý : `document.querySelector` và `document.querySelectorAll` hoạt động khá **CHẬM**, hãy thử dùng `getElementById`, `document.getElementsByClassName` hoặc `document.getElementsByTagName` nếu bạn muốn đạt hiệu suất tốt hơn. + +- [1.0](#1.0) Query bằng selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query bằng class + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // hoặc + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query bằng id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // hoặc + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query bằng thuộc tính + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Tìm bất cứ gì. + + + Tìm node + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Tìm body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + lấy thuộc tính + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Lấy giá trị của thuộc tính `data` + + ```js + // jQuery + $el.data('foo'); + + // Native + // using getAttribute + el.getAttribute('data-foo'); + // you can also use `dataset` if only need to support IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Tìm element cùng level/trước/sau + + + Element cùng level + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Element ở phía trước + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Element ở phía sau + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Element gần nhất + + Trả về element đầu tiên có selector khớp với yêu cầu khi duyệt từ element hiện tại trở lên tới document. + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Tìm parent + + Truy ngược một cách đệ quy tổ tiên của element hiện tại, cho đến khi tìm được một element tổ tiên ( element cần tìm ) mà element đó là con trực tiếp của element khớp với selector được cung cấp, Return lại element cần tìm đó. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Lấy index của e.currentTarget trong danh sách các element khớp với selector `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Nội dung Iframe + + `$('iframe').contents()` trả về thuộc tính `contentDocument` của iframe được tìm thấy + + + Nọi dung iframe + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Query Iframe + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ Trở về đầu](#danh-mục)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Lấy style + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: Bug đã được biết, sẽ trả về 'auto' nếu giá trị của style là 'auto' + const win = el.ownerDocument.defaultView; + // null means not return presudo styles + win.getComputedStyle(el, null).color; + ``` + + + Đặt style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Lấy/Đặt Nhiều style + + Nếu bạn muốn đặt nhiều style một lần, bạn có thể sẽ thích phương thức [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) trong thư viện oui-dom-utils. + + + + Thêm class và element + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Loại bỏ class class ra khỏi element + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + Kiểm tra xem element có class nào đó hay không + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Chiều rộng, chiều cao + + Về mặt lý thuyết thì chiều rộng và chiều cao giống như nhau trong cả jQuery và DOM API: + + + Chiều rộng của window + + ```js + // window height + $(window).height(); + // trừ đi scrollbar + window.document.documentElement.clientHeight; + // Tính luôn scrollbar + window.innerHeight; + ``` + + + Chiều cao của Document + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Chiều cao của element + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // chính xác tới số nguyên(khi có thuộc tính `box-sizing` là `border-box`, nó là `height`; khi box-sizing là `content-box`, nó là `height + padding + border`) + el.clientHeight; + // Chính xác tới số thập phân(khi `box-sizing` là `border-box`, nó là `height`; khi `box-sizing` là `content-box`, nó là `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ Trở về đầu](#danh-mục)** + +## Thao tác với DOM + +- [3.1](#3.1) Loại bỏ + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Lấy text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Đặt giá trị text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Lấy HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Đặt giá trị HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + append một element sau element con cuối cùng của element cha + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Chèn một node vào trước element được query. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Chèn node vào sau element được query + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ Trở về đầu](#danh-mục)** + +## Ajax + +Thay thế bằng [fetch](https://github.com/camsong/fetch-ie8) và [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ Trở về đầu](#danh-mục)** + +## Events + +Để có một sự thay thế đầy đủ nhất, bạn nên sử dụng https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind event bằng on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind event bằng off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ Trở về đầu](#danh-mục)** + +## Hàm tiện ích + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Mở rộng, sử dụng object.assign https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ Trở về đầu](#danh-mục)** + +## Ngôn ngữ khác + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Các trình duyệt hỗ trợ + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Giấy phép + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md new file mode 100644 index 000000000..54455115e --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md @@ -0,0 +1,709 @@ +## You Don't Need jQuery + +오늘날 프론트엔드 개발 환경은 급격히 진화하고 있고, 모던 브라우저들은 이미 충분히 많은 DOM/BOM API들을 구현했습니다. 우리는 jQuery를 DOM 처리나 이벤트를 위해 처음부터 배울 필요가 없습니다. React, Angular, Vue같은 프론트엔드 라이브러리들이 주도권을 차지하는 동안 DOM을 바로 처리하는 것은 안티패턴이 되었고, jQuery의 중요성은 줄어들었습니다. 이 프로젝트는 대부분의 jQuery 메소드의 대안을 IE 10+ 이상을 지원하는 네이티브 구현으로 소개합니다. + +## 목차 + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [DOM 조작](#dom-조작) +1. [Ajax](#ajax) +1. [이벤트](#이벤트) +1. [유틸리티](#유틸리티) +1. [대안방법](#대안방법) +1. [번역](#번역) +1. [브라우저 지원](#브라우저-지원) + +## Query Selector + +평범한 class, id, attribute같은 selecotor는 `document.querySelector`나 `document.querySelectorAll`으로 대체할 수 있습니다. +* `document.querySelector`는 처음 매칭된 엘리먼트를 반환합니다. +* `document.querySelectorAll`는 모든 매칭된 엘리먼트를 NodeList로 반환합니다. `[].slice.call`을 사용해서 Array로 변환할 수 있습니다. +* 만약 매칭된 엘리멘트가 없으면 jQuery는 `[]` 를 반환하지만 DOM API는 `null`을 반환합니다. Null Pointer Exception에 주의하세요. + +> 안내: `document.querySelector`와 `document.querySelectorAll`는 꽤 **느립니다**, `getElementById`나 `document.getElementsByClassName`, `document.getElementsByTagName`를 사용하면 퍼포먼스가 향상을 기대할 수 있습니다. + +- [1.0](#1.0) selector로 찾기 + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) class로 찾기 + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) id로 찾기 + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) 속성(attribute)으로 찾기 + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) 자식에서 찾기 + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + +- [1.5](#1.5) 형제/이전/다음 엘리먼트 찾기 + + + 형제 엘리먼트 + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + 이전 엘리먼트 + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + ``` + + + 다음 엘리먼트 + + ```js + // jQuery + $el.next(); + + // Native + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + 현재 엘리먼트부터 document로 이동하면서 주어진 셀렉터와 일치하는 가장 가까운 엘리먼트를 반환합니다. + + ```js + // jQuery + $el.closest(selector); + + // Native - 최신 브라우저만, IE는 미지원 + el.closest(selector); + + // Native - IE10 이상 + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + 주어진 셀렉터에 매칭되는 엘리먼트를 찾기까지 부모 태그들을 위로 올라가며 탐색하여 저장해두었다가 DOM 노드 또는 jQuery object로 반환합니다. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + e.currentTarget이 `.radio`의 몇번째인지 구하기 + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()`는 iframe에 한정해서 `contentDocument`를 반환합니다. + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe에서 찾기 + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +- [1.10](#1.10) body 얻기 + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + +- [1.11](#1.11) 속성 얻기 및 설정 + + + 속성 얻기 + + ```js + // jQuery + $el.attr('foo'); + + // Native + el.getAttribute('foo'); + ``` + + 속성 설정하기 + + ```js + // jQuery, DOM 변형 없이 메모리에서 작동됩니다. + $el.attr('foo', 'bar'); + + // Native + el.setAttribute('foo', 'bar'); + ``` + + + `data-` 속성 얻기 + + ```js + // jQuery + $el.data('foo'); + + // Native (`getAttribute` 사용) + el.getAttribute('data-foo'); + // Native (IE 11 이상의 지원만 필요하다면 `dataset`을 사용) + el.dataset['foo']; + ``` + +**[⬆ 목차로 돌아가기](#목차)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + style값 얻기 + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: 알려진 버그로, style값이 'auto'이면 'auto'를 반환합니다. + const win = el.ownerDocument.defaultView; + // null은 가상 스타일은 반환하지 않음을 의미합니다. + win.getComputedStyle(el, null).color; + ``` + + + style값 설정하기 + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Style값들을 동시에 얻거나 설정하기 + + 만약 한번에 여러 style값을 바꾸고 싶다면 oui-dom-utils 패키지의 [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194)를 사용해보세요. + + + + class 추가하기 + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + class 제거하기 + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + class를 포함하고 있는지 검사하기 + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + class 토글하기 + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) 폭과 높이 + + 폭과 높이는 이론상 동일합니다. 높이로 예를 들겠습니다. + + + Window의 높이 + + ```js + // window 높이 + $(window).height(); + // jQuery처럼 스크롤바를 제외하기 + window.document.documentElement.clientHeight; + // 스크롤바 포함 + window.innerHeight; + ``` + + + 문서 높이 + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element 높이 + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // 정수로 정확하게(`border-box`일 때 이 값은 `height`이고, `content-box`일 때, 이 값은 `height + padding + border`) + el.clientHeight; + // 실수로 정확하게(`border-box`일 때 이 값은 `height`이고, `content-box`일 때, 이 값은 `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ 목차로 돌아가기](#목차)** + +## DOM 조작 + +- [3.1](#3.1) 제거 + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + text 가져오기 + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + text 설정하기 + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + HTML 가져오기 + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + HTML 설정하기 + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) 해당 엘리먼트의 자식들 뒤에 넣기(Append) + + 부모 엘리먼트의 마지막 자식으로 엘리먼트를 추가합니다. + + ```js + // jQuery + $el.append("
hello
"); + + // Native + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) 해당 엘리먼트의 자식들 앞에 넣기(Prepend) + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native +el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) 해당 엘리먼트 앞에 넣기(insertBefore) + + 새 노드를 선택한 엘리먼트 앞에 넣습니다. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) 해당 엘리먼트 뒤에 넣기(insertAfter) + + 새 노드를 선택한 엘리먼트 뒤에 넣습니다. + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` +- [3.8](#3.8) is + + query selector와 일치하면 `true` 를 반환합니다. + + ```js + // jQuery + $el.is(selector); + + // Native + el.matches(selector); + ``` + +- [3.9](#3.9) clone + + 엘리먼트의 복제본을 만듭니다. + + ```js + // jQuery + $el.clone(); + + // Native + el.cloneNode(); + + // Deep clone은 파라미터를 `true` 로 설정하세요. + ``` + +- [3.10](#3.10) empty + + 모든 자식 노드를 제거합니다. + + ```js + // jQuery + $el.empty(); + + // Native + el.innerHTML = ''; + ``` + +**[⬆ 목차로 돌아가기](#목차)** + +## Ajax + +[Fetch API](https://fetch.spec.whatwg.org/) 는 XMLHttpRequest를 ajax로 대체하는 새로운 표준 입니다. Chrome과 Firefox에서 작동하며, polyfill을 이용해서 구형 브라우저에서 작동되도록 만들 수도 있습니다. + +IE9 이상에서 지원하는 [github/fetch](http://github.com/github/fetch) 혹은 IE8 이상에서 지원하는 [fetch-ie8](https://github.com/camsong/fetch-ie8/), JSONP 요청을 만드는 [fetch-jsonp](https://github.com/camsong/fetch-jsonp)를 이용해보세요. + +**[⬆ 목차로 돌아가기](#목차)** + +## 이벤트 + +namespace와 delegation을 포함해서 완전히 갈아 엎길 원하시면 https://github.com/oneuijs/oui-dom-events 를 고려해보세요. + +- [5.1](#5.1) 이벤트 Bind 걸기 + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) 이벤트 Bind 풀기 + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) 이벤트 발생시키기(Trigger) + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ 목차로 돌아가기](#목차)** + +## 유틸리티 + +- [6.1](#6.1) 배열인지 검사(isArray) + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) 앞뒤 공백 지우기(Trim) + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + 사용하려면 object.assign polyfill을 사용하세요. https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +- [6.5](#6.5) inArray + + ```js + // jQuery + $.inArray(item, array); + + // Native + array.indexOf(item); + ``` + +- [6.6](#6.6) map + + ```js + // jQuery + $.map(array, function(value, index) { + }); + + // Native + Array.map(function(value, index) { + }); + ``` + +**[⬆ 목차로 돌아가기](#목차)** + +## 대안방법 + +* [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - 일반 자바스크립트로 공통이벤트, 엘리먼트, ajax 등을 다루는 방법 예제. +* [npm-dom](http://github.com/npm-dom) 과 [webmodules](http://github.com/webmodules) - 개별 DOM모듈을 NPM에서 찾을 수 있습니다. + +## 번역 + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) +* [Italian](./README-it.md) + +## 브라우저 지원 + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md new file mode 100644 index 000000000..6476f5034 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md @@ -0,0 +1,1190 @@ +## You Don't Need jQuery [![Build Status](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery.svg)](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery) + +Frontend environments evolve rapidly nowadays, modern browsers have already implemented a great deal of DOM/BOM APIs which are good enough. We don't have to learn jQuery from scratch for DOM manipulation or events. In the meantime, thanks to the prevailment of frontend libraries such as React, Angular and Vue, manipulating DOM directly becomes anti-pattern, jQuery has never been less important. This project summarizes most of the jQuery method alternatives in native implementation, with IE 10+ support. + +## Table of Contents + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [DOM Manipulation](#dom-manipulation) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utilities](#utilities) +1. [Promises](#promises) +1. [Animation](#animation) +1. [Alternatives](#alternatives) +1. [Translations](#translations) +1. [Browser Support](#browser-support) + +## Query Selector + +In place of common selectors like class, id or attribute we can use `document.querySelector` or `document.querySelectorAll` for substitution. The differences lie in: +* `document.querySelector` returns the first matched element +* `document.querySelectorAll` returns all matched elements as NodeList. It can be converted to Array using `[].slice.call(document.querySelectorAll(selector) || []);` +* If no elements matched, jQuery would return `[]` whereas the DOM API will return `null`. Pay attention to Null Pointer Exception. You can also use `||` to set default value if not found, like `document.querySelectorAll(selector) || []` + +> Notice: `document.querySelector` and `document.querySelectorAll` are quite **SLOW**, try to use `getElementById`, `document.getElementsByClassName` or `document.getElementsByTagName` if you want to get a performance bonus. + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query by class + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query by id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query by attribute + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Query in descendents + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + ``` + + + Next elements + + ```js + // jQuery + $el.next(); + + // Native + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Return the first matched element by provided selector, traversing from current element to document. + + ```js + // jQuery + $el.closest(selector); + + // Native - Only latest, NO IE + el.closest(selector); + + // Native - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` returns `contentDocument` for this specific iframe + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +- [1.10](#1.10) Get body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + +- [1.11](#1.11) Attribute getter and setter + + + Get an attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + el.getAttribute('foo'); + ``` + + Set an attribute + + ```js + // jQuery, note that this works in memory without change the DOM + $el.attr('foo', 'bar'); + + // Native + el.setAttribute('foo', 'bar'); + ``` + + + Get a `data-` attribute + + ```js + // jQuery + $el.data('foo'); + + // Native (use `getAttribute`) + el.getAttribute('data-foo'); + // Native (use `dataset` if only need to support IE 11+) + el.dataset['foo']; + ``` + +**[⬆ back to top](#table-of-contents)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: Known bug, will return 'auto' if style value is 'auto' + const win = el.ownerDocument.defaultView; + // null means not return pseudo styles + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Note that if you want to set multiple styles once, you could refer to [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) method in oui-dom-utils package. + + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width and Height are theoretically identical, take Height as example: + + + Window height + + ```js + // window height + $(window).height(); + // without scrollbar, behaves like jQuery + window.document.documentElement.clientHeight; + // with scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#table-of-contents)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Append child element after the last child of parent element + + ```js + // jQuery + $el.append("
hello
"); + + // Native + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Insert a new node before the selected elements + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insert a new node after the selected elements + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) is + + Return `true` if it matches the query selector + + ```js + // jQuery - Notice `is` also work with `function` or `elements` which is not concerned here + $el.is(selector); + + // Native + el.matches(selector); + ``` +- [3.9](#3.9) clone + + Create a deep copy of that element + + ```js + // jQuery + $el.clone(); + + // Native + el.cloneNode(); + + // For Deep clone , set param as `true` + ``` + +- [3.10](#3.10) empty + + Remove all child nodes + + ```js + // jQuery + $el.empty(); + + // Native + el.innerHTML = ''; + ``` + +- [3.11](#3.11) wrap + + Wrap an HTML structure around each element + + ```js + // jQuery + $('.inner').wrap('
'); + + // Native + [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ + var wrapper = document.createElement('div'); + wrapper.className = 'wrapper'; + el.parentNode.insertBefore(wrapper, el); + el.parentNode.removeChild(el); + wrapper.appendChild(el); + }); + ``` + +- [3.12](#3.12) unwrap + + Remove the parents of the set of matched elements from the DOM + + ```js + // jQuery + $('.inner').unwrap(); + + // Native + [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ + [].slice.call(el.childNodes).forEach(function(child){ + el.parentNode.insertBefore(child, el); + }); + el.parentNode.removeChild(el); + }); + ``` + + - [3.13](#3.13) replaceWith + + Replace each element in the set of matched elements with the provided new content + + ```js + // jQuery + $('.inner').replaceWith('
'); + + // Native + [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ + var outer = document.createElement('div'); + outer.className = 'outer'; + el.parentNode.insertBefore(outer, el); + el.parentNode.removeChild(el); + }); + ``` + + +**[⬆ back to top](#table-of-contents)** + +## Ajax + +[Fetch API](https://fetch.spec.whatwg.org/) is the new standard to replace XMLHttpRequest to do ajax. It works on Chrome and Firefox, you can use polyfills to make it work on legacy browsers. + +Try [github/fetch](http://github.com/github/fetch) on IE9+ or [fetch-ie8](https://github.com/camsong/fetch-ie8/) on IE8+, [fetch-jsonp](https://github.com/camsong/fetch-jsonp) to make JSONP requests. + +**[⬆ back to top](#table-of-contents)** + +## Events + +For a complete replacement with namespace and delegation, refer to https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind an event with on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Utilities + +Most of utilities are found by native API. Others advanced functions could be choosed better utilities library focus on consistency and performance. Recommend [lodash](https://lodash.com) to replace. + +- [6.1](#6.1) Basic utilities + + + isArray + + Determine whether the argument is an array. + + ```js + // jQuery + $.isArray(array); + + // Native + Array.isArray(array); + ``` + + + isWindow + + Determine whether the argument is a window. + + ```js + // jQuery + $.isArray(obj); + + // Native + function isWindow(obj) { + return obj != null && obj === obj.window; + } + ``` + + + inArray + + Search for a specified value within an array and return its index (or -1 if not found). + + ```js + // jQuery + $.inArray(item, array); + + // Native + Array.indexOf(item); + ``` + + + isNumbic + + Determines whether its argument is a number. + Use `typeof` to decide type. if necessary to use library, sometimes `typeof` isn't accurate. + + ```js + // jQuery + $.isNumbic(item); + + // Native + function isNumbic(item) { + return typeof value === 'number'; + } + ``` + + + isFunction + + Determine if the argument passed is a JavaScript function object. + + ```js + // jQuery + $.isFunction(item); + + // Native + function isFunction(item) { + return typeof value === 'function'; + } + ``` + + + isEmptyObject + + Check to see if an object is empty (contains no enumerable properties). + + ```js + // jQuery + $.isEmptyObject(obj); + + // Native + function isEmptyObject(obj) { + for (let key in obj) { + return false; + } + return true; + } + ``` + + + isPlanObject + + Check to see if an object is a plain object (created using “{}” or “new Object”). + + ```js + // jQuery + $.isPlanObject(obj); + + // Native + function isPlainObject(obj) { + if (typeof (obj) !== 'object' || obj.nodeType || obj != null && obj === obj.window) { + return false; + } + + if (obj.constructor && + !{}.hasOwnPropert.call(obj.constructor.prototype, 'isPrototypeOf')) { + return false; + } + + return true; + } + ``` + + + extend + + Merge the contents of two or more objects together into the first object. + object.assign is ES6 API, and you could use [polyfill](https://github.com/ljharb/object.assign) also. + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + + + trim + + Remove the whitespace from the beginning and end of a string. + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + + + map + + Translate all items in an array or object to new array of items. + + ```js + // jQuery + $.map(array, function(value, index) { + }); + + // Native + array.map(function(value, index) { + }); + ``` + + + each + + A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. + + ```js + // jQuery + $.each(array, function(value, index) { + }); + + // Native + array.forEach(function(value, index) { + }); + ``` + + + grep + + Finds the elements of an array which satisfy a filter function. + + ```js + // jQuery + $.grep(array, function(value, index) { + }); + + // Native + array.filter(function(value, index) { + }); + ``` + + + type + + Determine the internal JavaScript [[Class]] of an object. + + ```js + // jQuery + $.type(obj); + + // Native + Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1').toLowerCase(); + ``` + + + merge + + Merge the contents of two arrays together into the first array. + + ```js + // jQuery + $.merge(array1, array2); + + // Native + // But concat function don't remove duplicate items. + function merge() { + return Array.prototype.concat.apply([], arguments) + } + ``` + + + now + + Return a number representing the current time. + + ```js + // jQuery + $.now(); + + // Native + Date.now(); + ``` + + + proxy + + Takes a function and returns a new one that will always have a particular context. + + ```js + // jQuery + $.proxy(fn, context); + + // Native + fn.bind(context); + ``` + + + makeArray + + Convert an array-like object into a true JavaScript array. + + ```js + // jQuery + $.makeArray(array); + + // Native + [].slice.call(array); + ``` + +- [6.2](#6.2) DOM utilities + + + unique + + Sorts an array of DOM elements, in place, with the duplicates removed. Note that this only works on arrays of DOM elements, not strings or numbers. + + Sizzle's API + + + contains + + Check to see if a DOM element is a descendant of another DOM element. + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +- [6.3](#6.3) Globaleval + + ```js + // jQuery + $.globaleval(code); + + // Native + function Globaleval(code) { + let script = document.createElement('script'); + script.text = code; + + document.head.appendChild(script).parentNode.removeChild(script); + } + + // Use eval, but context of eval is current, context of $.Globaleval is global. + eval(code); + ``` + +- [6.4](#6.4) parse + + + parseHTML + + Parses a string into an array of DOM nodes. + + ```js + // jQuery + $.parseHTML(htmlString); + + // Native + function parseHTML(string) { + const tmp = document.implementation.createHTMLDocument(); + tmp.body.innerHTML = string; + return tmp.body.children; + } + ``` + + + parseJSON + + Takes a well-formed JSON string and returns the resulting JavaScript value. + + ```js + // jQuery + $.parseJSON(str); + + // Native + JSON.parse(str); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Promises + +A promise represents the eventual result of an asynchronous operation. jQuery has its own way to handle promises. Native JavaScript implements a thin and minimal API to handle promises according to the [Promises/A+](http://promises-aplus.github.io/promises-spec/) specification. + +- [7.1](#7.1) done, fail, always + + `done` is called when promise is resolved, `fail` is called when promise is rejected, `always` is called when promise is either resolved or rejected. + + ```js + // jQuery + $promise.done(doneCallback).fail(failCallback).always(alwaysCallback) + + // Native + promise.then(doneCallback, failCallback).then(alwaysCallback, alwaysCallback) + ``` + +- [7.2](#7.2) when + + `when` is used to handle multiple promises. It will resolve when all promises are resolved, and reject if either one is rejected. + + ```js + // jQuery + $.when($promise1, $promise2).done((promise1Result, promise2Result) => {}) + + // Native + Promise.all([$promise1, $promise2]).then([promise1Result, promise2Result] => {}); + ``` + +- [7.3](#7.3) Deferred + + Deferred is a way to create promises. + + ```js + // jQuery + function asyncFunc() { + var d = new $.Deferred(); + setTimeout(function() { + if(true) { + d.resolve('some_value_compute_asynchronously'); + } else { + d.reject('failed'); + } + }, 1000); + return d.promise(); + } + + // Native + function asyncFunc() { + return new Promise((resolve, reject) => { + setTimeout(function() { + if (true) { + resolve('some_value_compute_asynchronously'); + } else { + reject('failed'); + } + }, 1000); + }); + } + ``` + +**[⬆ back to top](#table-of-contents)** + +## Animation + +- [8.1](#8.1) Show & Hide + + ```js + // jQuery + $el.show(); + $el.hide(); + + // Native + // More detail about show method, please refer to https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L363 + el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block'; + el.style.display = 'none'; + ``` + +- [8.2](#8.2) Toggle + + ```js + // jQuery + $el.toggle(); + + // Native + if (el.ownerDocument.defaultView.getComputedStyle(el, null).display === 'none') { + el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block'; + } + else { + el.style.display = 'none'; + } + ``` + +- [8.3](#8.3) FadeIn & FadeOut + + ```js + // jQuery + $el.fadeIn(3000); + $el.fadeOut(3000); + + // Native + el.style.transition = 'opacity 3s'; + // fadeIn + el.style.opacity = '1'; + // fadeOut + el.style.opacity = '0'; + ``` + +- [8.4](#8.4) FadeTo + + ```js + // jQuery + $el.fadeTo('slow',0.15); + // Native + el.style.transition = 'opacity 3s'; // assume 'slow' equals 3 seconds + el.style.opacity = '0.15'; + ``` + +- [8.5](#8.5) FadeToggle + + ```js + // jQuery + $el.fadeToggle(); + + // Native + el.style.transition = 'opacity 3s'; + let { opacity } = el.ownerDocument.defaultView.getComputedStyle(el, null); + if (opacity === '1') { + el.style.opacity = '0'; + } + else { + el.style.opacity = '1'; + } + ``` + +- [8.6](#8.6) SlideUp & SlideDown + + ```js + // jQuery + $el.slideUp(); + $el.slideDown(); + + // Native + let originHeight = '100px'; + el.style.transition = 'height 3s'; + // slideUp + el.style.height = '0px'; + // slideDown + el.style.height = originHeight; + ``` + +- [8.7](#8.7) SlideToggle + + ```js + // jQuery + $el.slideToggle(); + + // Native + let originHeight = '100px'; + el.style.transition = 'height 3s'; + let { height } = el.ownerDocument.defaultView.getComputedStyle(el, null); + if (parseInt(height, 10) === 0) { + el.style.height = originHeight; + } + else { + el.style.height = '0px'; + } + ``` + +- [8.8](#8.8) Animate + + ```js + // jQuery + $el.animate({params}, speed); + + // Native + el.style.transition = 'all' + speed; + Object.keys(params).forEach(function(key) { + el.style[key] = params[key]; + }) + ``` + +## Alternatives + +* [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - Examples of how to do common event, element, ajax etc with plain javascript. +* [npm-dom](http://github.com/npm-dom) and [webmodules](http://github.com/webmodules) - Organizations you can find individual DOM modules on NPM + +## Translations + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) +* [Italian](./README-it.md) + +## Browser Support + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md new file mode 100644 index 000000000..e576f0ec4 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md @@ -0,0 +1,628 @@ +> #### You Don't Need jQuery + +Você não precisa de jQuery +--- + +Ambientes Frontend evoluem rapidamente nos dias de hoje, navegadores modernos já implementaram uma grande parte das APIs DOM/BOM que são boas o suficiente. Nós não temos que aprender jQuery a partir do zero para manipulação do DOM ou eventos. Nesse meio tempo, graças a bibliotecas frontend como React, Angular e Vue, a manipulação direta do DOM torna-se um anti-padrão, jQuery é menos importante do que nunca. Este projeto resume a maioria das alternativas dos métodos jQuery em implementação nativa, com suporte ao IE 10+. + +## Tabela de conteúdos + +1. [Query Selector](#query-selector) +1. [CSS & Estilo](#css--estilo) +1. [Manipulação do DOM](#manipulação-do-dom) +1. [Ajax](#ajax) +1. [Eventos](#eventos) +1. [Utilitários](#utilitários) +1. [Suporte dos Navegadores](#suporte-dos-navegadores) + +## Query Selector + +No lugar de seletores comuns como classe, id ou atributo podemos usar `document.querySelector` ou `document.querySelectorAll` para substituição. As diferenças são: +* `document.querySelector` retorna o primeiro elemento correspondente +* `document.querySelectorAll` retorna todos os elementos correspondentes como NodeList. Pode ser convertido para Array usando `[].slice.call(document.querySelectorAll(selector) || []);` +* Se não tiver elementos correspondentes, jQuery retornaria `[]` considerando que a DOM API irá retornar `null`. Preste atenção ao Null Pointer Exception. Você também pode usar `||` para definir um valor padrão caso nenhum elemento seja encontrado, como `document.querySelectorAll(selector) || []` + +> Aviso: `document.querySelector` e `document.querySelectorAll` são bastante **LENTOS**, tente usar `getElementById`, `document.getElementsByClassName` ou `document.getElementsByTagName` se você quer ter uma maior performance. + +- [1.0](#1.0) Query por seletor + + ```js + // jQuery + $('selector'); + + // Nativo + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query por classe + + ```js + // jQuery + $('.class'); + + // Nativo + document.querySelectorAll('.class'); + + // ou + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query por id + + ```js + // jQuery + $('#id'); + + // Nativo + document.querySelector('#id'); + + // ou + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query por atributo + + ```js + // jQuery + $('a[target=_blank]'); + + // Nativo + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Find sth. + + + Busca por nós + + ```js + // jQuery + $el.find('li'); + + // Nativo + el.querySelectorAll('li'); + ``` + + + Buscar `body` + + ```js + // jQuery + $('body'); + + // Nativo + document.body; + ``` + + + Buscar atributos + + ```js + // jQuery + $el.attr('foo'); + + // Nativo + e.getAttribute('foo'); + ``` + + + Buscar atributos `data-` + + ```js + // jQuery + $el.data('foo'); + + // Nativo + // usando getAttribute + el.getAttribute('data-foo'); + // você também pode usar `dataset` se você precisar suportar apenas IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Nativo + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Nativo + el.previousElementSibling; + + ``` + + + Next elements + + ```js + // jQuery + $el.next(); + + // Nativo + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Retorna o primeiro elemento que corresponda ao seletor, partindo do elemento atual para o document. + + ```js + // jQuery + $el.closest(queryString); + + // Nativo + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Obtém os ancestrais de cada elemento no atual conjunto de elementos combinados, mas não inclui o elemento correspondente pelo seletor, nó do DOM, ou objeto jQuery. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Nativo + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Nativo + document.querySelector('#my-input').value; + ``` + + + Obter o índice do e.currentTarget entre `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Nativo + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` retorna `contentDocument` para este iframe específico + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Nativo + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Nativo + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + + +## CSS & Estilo + +- [2.1](#2.1) CSS + + + Obter estilo + + ```js + // jQuery + $el.css("color"); + + // Nativo + // AVISO: Bug conhecido, irá retornar 'auto' se o valor do estilo for 'auto' + const win = el.ownerDocument.defaultView; + // null significa não retornar estilos + win.getComputedStyle(el, null).color; + ``` + + + Definir Estilo + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Nativo + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Observe que se você deseja setar vários estilos de uma vez, você pode optar por [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) método no pacote oui-dom-utils. + + + + Adicionar classe + + ```js + // jQuery + $el.addClass(className); + + // Nativo + el.classList.add(className); + ``` + + + Remover classe + + ```js + // jQuery + $el.removeClass(className); + + // Nativo + el.classList.remove(className); + ``` + + + Verificar classe + + ```js + // jQuery + $el.hasClass(className); + + // Nativo + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Nativo + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Largura e Altura + + `width` e `height` são teoricamente idênticos, vamos pegar `height` como exemplo: + + + Altura da janela + + ```jsc + // window height + $(window).height(); + // sem scrollbar, se comporta como jQuery + window.document.documentElement.clientHeight; + // com scrollbar + window.innerHeight; + ``` + + + Altura do Documento + + ```js + // jQuery + $(document).height(); + + // Nativo + document.documentElement.scrollHeight; + ``` + + + Altura do Elemento + + ```js + // jQuery + $el.height(); + + // Nativo + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // preciso para inteiro(quando `border-box`, é `height`; quando `content-box`, é `height + padding + border`) + el.clientHeight; + // preciso para decimal(quando `border-box`, é `height`; quando `content-box`, é `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Nativo + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Nativo + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Rolar para o topo + + ```js + // jQuery + $(window).scrollTop(); + + // Nativo + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Manipulação do Dom + +- [3.1](#3.1) Remover + ```js + // jQuery + $el.remove(); + + // Nativo + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Texto + + + Obter texto + + ```js + // jQuery + $el.text(); + + // Nativo + el.textContent; + ``` + + + Definir texto + + ```js + // jQuery + $el.text(string); + + // Nativo + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Obter HTML + + ```js + // jQuery + $el.html(); + + // Nativo + el.innerHTML; + ``` + + + Definir HTML + + ```js + // jQuery + $el.html(htmlString); + + // Nativo + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Incluir elemento filho após o último filho do elemento pai. + + ```js + // jQuery + $el.append("
hello
"); + + // Nativo + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Nativo + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Insere um novo nó antes dos elementos selecionados. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insere um novo nó após os elementos selecionados. + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Ajax + +Substitua por [fetch](https://github.com/camsong/fetch-ie8) e [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Eventos + +Para uma substituição completa com namespace e delegation, consulte https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) `Bind` num evento com `on` + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Nativo + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) `Unbind` num evento com `off` + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Nativo + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Nativo + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Utilitários + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Nativo + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Nativo + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Use o polyfill `object.assign` para eetender um Object: https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Nativo + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Nativo + el !== child && el.contains(child); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Suporte dos Navegadores + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Licença + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md new file mode 100644 index 000000000..24d6f8795 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md @@ -0,0 +1,656 @@ +## You Don't Need jQuery + +前端发展很快,现代浏览器原生 API 已经足够好用。我们并不需要为了操作 DOM、Event 等再学习一下 jQuery 的 API。同时由于 React、Angular、Vue 等框架的流行,直接操作 DOM 不再是好的模式,jQuery 使用场景大大减少。本项目总结了大部分 jQuery API 替代的方法,暂时只支持 IE10+ 以上浏览器。 + +## 目录 + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [DOM Manipulation](#dom-manipulation) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utilities](#utilities) +1. [Alternatives](#alternatives) +1. [Translations](#translations) +1. [Browser Support](#browser-support) + +## Query Selector + +常用的 class、id、属性 选择器都可以使用 `document.querySelector` 或 `document.querySelectorAll` 替代。区别是 +* `document.querySelector` 返回第一个匹配的 Element +* `document.querySelectorAll` 返回所有匹配的 Element 组成的 NodeList。它可以通过 `[].slice.call()` 把它转成 Array +* 如果匹配不到任何 Element,jQuery 返回空数组 `[]`,但 `document.querySelector` 返回 `null`,注意空指针异常。当找不到时,也可以使用 `||` 设置默认的值,如 `document.querySelectorAll(selector) || []` + +> 注意:`document.querySelector` 和 `document.querySelectorAll` 性能很**差**。如果想提高性能,尽量使用 `document.getElementById`、`document.getElementsByClassName` 或 `document.getElementsByTagName`。 + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query by class + + ```js + // jQuery + $('.css'); + + // Native + document.querySelectorAll('.css'); + + // or + document.getElementsByClassName('css'); + ``` + +- [1.2](#1.2) Query by id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query by attribute + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Find sth. + + + Find nodes + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Find body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + Find Attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Find data attribute + + ```js + // jQuery + $el.data('foo'); + + // Native + // using getAttribute + el.getAttribute('data-foo'); + // you can also use `dataset` if only need to support IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Next elements + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Closest 获得匹配选择器的第一个祖先元素,从当前元素开始沿 DOM 树向上。 + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + 获取当前每一个匹配元素集的祖先,不包括匹配元素的本身。 + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + jQuery 对象的 iframe `contents()` 返回的是 iframe 内的 `document` + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ 回到顶部](#目录)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // 注意:此处为了解决当 style 值为 auto 时,返回 auto 的问题 + const win = el.ownerDocument.defaultView; + // null 的意思是不返回伪类元素 + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + 注意,如果想一次设置多个 style,可以参考 oui-dom-utils 中 [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) 方法 + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width 与 Height 获取方法相同,下面以 Height 为例: + + + Window height + + ```js + // jQuery + $(window).height(); + + // Native + // 不含 scrollbar,与 jQuery 行为一致 + window.document.documentElement.clientHeight; + // 含 scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + // 与 jQuery 一致(一直为 content 区域的高度) + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // 精确到整数(border-box 时为 height 值,content-box 时为 height + padding + border 值) + el.clientHeight; + // 精确到小数(border-box 时为 height 值,content-box 时为 height + padding + border 值) + el.getBoundingClientRect().height; + ``` + + + Iframe height + + $iframe .contents() 方法返回 iframe 的 contentDocument + + ```js + // jQuery + $('iframe').contents().height(); + + // Native + iframe.contentDocument.documentElement.scrollHeight; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ 回到顶部](#目录)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Append 插入到子节点的末尾 + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + 在选中元素前插入新节点 + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + 在选中元素后插入新节点 + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ 回到顶部](#目录)** + +## Ajax + +用 [fetch](https://github.com/camsong/fetch-ie8) 和 [fetch-jsonp](https://github.com/camsong/fetch-jsonp) 替代 + +**[⬆ 回到顶部](#目录)** + +## Events + +完整地替代命名空间和事件代理,链接到 https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind an event with on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ 回到顶部](#目录)** + +## Utilities + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + 继承,使用 object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ 回到顶部](#目录)** + +## Alternatives + +* [你可能不需要 jQuery (You Might Not Need jQuery)](http://youmightnotneedjquery.com/) - 如何使用原生 JavaScript 实现通用事件,元素,ajax 等用法。 +* [npm-dom](http://github.com/npm-dom) 以及 [webmodules](http://github.com/webmodules) - 在 NPM 上提供独立 DOM 模块的组织 + +## Translations + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Русский](./README-ru.md) +* [Türkçe](./README-tr.md) +* [Italian](./README-it.md) + +## Browser Support + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git new file mode 100644 index 000000000..c115085f7 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git @@ -0,0 +1 @@ +gitdir: ../.git/modules/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js new file mode 100644 index 000000000..f935ae405 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js @@ -0,0 +1,92 @@ +// Karma configuration +// Generated on Sun Nov 22 2015 22:10:47 GMT+0800 (CST) +require('babel-core/register'); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '.', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + // list of files / patterns to load in the browser + files: [ + './test/**/*.spec.js' + ], + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/**/*.spec.js': ['webpack', 'sourcemap'] + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + coverageReporter: { + reporters: [ + {type: 'text'}, + {type: 'html', dir: 'coverage'}, + ] + }, + + webpackMiddleware: { + stats: 'minimal' + }, + + webpack: { + cache: true, + devtool: 'inline-source-map', + module: { + loaders: [{ + test: /\.jsx?$/, + loader: 'babel-loader', + exclude: /node_modules/ + }], + postLoaders: [{ + test: /\.js/, + exclude: /(test|node_modules)/, + loader: 'istanbul-instrumenter' + }], + }, + resolve: { + extensions: ['', '.js', '.jsx'] + } + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Firefox'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + // singleRun: false, + + // Concurrency level + // how many browser should be started simultanous + // concurrency: Infinity, + + // plugins: ['karma-phantomjs-launcher', 'karma-sourcemap-loader', 'karma-webpack'] + }) +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json new file mode 100644 index 000000000..734e6ca90 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json @@ -0,0 +1,53 @@ +{ + "name": "You-Dont-Need-jQuery", + "version": "1.0.0", + "description": "Examples of how to do query, style, dom, ajax, event etc like jQuery with plain javascript.", + "scripts": { + "test": "karma start --single-run", + "tdd": "karma start --auto-watch --no-single-run", + "test-cov": "karma start --auto-watch --single-run --reporters progress,coverage", + "lint": "eslint src test" + }, + "dependencies": {}, + "devDependencies": { + "babel-cli": "^6.2.0", + "babel-core": "^6.1.21", + "babel-eslint": "^4.1.5", + "babel-loader": "^6.2.0", + "babel-preset-es2015": "^6.1.18", + "babel-preset-stage-0": "^6.1.18", + "chai": "^3.4.1", + "eslint": "^1.9.0", + "eslint-config-airbnb": "^1.0.0", + "eslint-plugin-react": "^3.10.0", + "isparta": "^4.0.0", + "istanbul-instrumenter-loader": "^0.1.3", + "jquery": "^2.1.4", + "karma": "^0.13.15", + "karma-coverage": "^0.5.3", + "karma-firefox-launcher": "^0.1.7", + "karma-mocha": "^0.2.1", + "karma-sourcemap-loader": "^0.3.6", + "karma-webpack": "^1.7.0", + "mocha": "^2.3.4", + "webpack": "^1.12.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/oneuijs/You-Dont-Need-jQuery.git" + }, + "keywords": [ + "convertion guide", + "jQuery", + "es6", + "es2015", + "babel", + "OneUI Group" + ], + "author": "OneUI Group", + "license": "MIT", + "bugs": { + "url": "https://github.com/oneuijs/You-Dont-Need-jQuery/issues" + }, + "homepage": "https://github.com/oneuijs/You-Dont-Need-jQuery" +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md new file mode 100644 index 000000000..f0f376887 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md @@ -0,0 +1,13 @@ +# Test cases for all the tips + +## Usage + +run all tests once +``` +npm run test +``` + +run tests on TDD(Test Driven Development) mode +``` +npm run tdd +``` diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js new file mode 100644 index 000000000..99248f2bb --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js @@ -0,0 +1 @@ +// test for CSS related \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js new file mode 100644 index 000000000..5bfc287e9 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js @@ -0,0 +1 @@ +// test for CSS and style related \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js new file mode 100644 index 000000000..f6b9c300e --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js @@ -0,0 +1,71 @@ +// tests for Query Selector related +import { expect } from 'chai'; +import $ from 'jquery'; + +describe('query selector', () => { + describe('basic', () => { + beforeEach(() => { + document.body.innerHTML = ` +
    +
  • I
  • +
  • II
  • +
  • III
  • +
  • +
      +
    • III.I
    • +
    • III.II
    • +
    +
  • +
+ `; + }); + + afterEach(() => { + const el = document.querySelector('#query-selector-test1'); + el.parentNode.removeChild(el); + }); + + it('1.0 Query by selector', () => { + const $els = $('li.item[data-role="red"]'); + const els = document.querySelectorAll('li.item[data-role="red"]'); + + expect($els.length).to.equal(2); + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + + it('1.1 Query by class', () => { + const $els = $('.item'); + const els = document.getElementsByClassName('item'); + + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + + it('1.2 Query by id', () => { + expect($('#nested-ul')[0]).to.equal(document.getElementById('nested-ul')); + }); + + it('1.3 Query by attribute', () => { + const $els = $('[data-role="blue"]'); + const els = document.querySelectorAll('[data-role="blue"]'); + + expect($els.length).to.equal(2); + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + + it('1.4 Query in descendents', () => { + const $els = $('#query-selector-test1').find('.item'); + const els = document.getElementById('query-selector-test1').querySelectorAll('.item'); + + expect($els.length).to.equal(4); + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + }); +}); \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js new file mode 100644 index 000000000..d68e723c5 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js @@ -0,0 +1 @@ +// test for Utilities related \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG b/spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG new file mode 100644 index 000000000..196384738 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG @@ -0,0 +1,9 @@ +submodules +# Please enter the commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +# On branch master +# Changes to be committed: +# new file: .gitmodules +# new file: You-Dont-Need-jQuery +# new file: jstips +# diff --git a/spec/fixtures/git/repo-with-submodules/git.git/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config new file mode 100644 index 000000000..ab57cc5f1 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/config @@ -0,0 +1,11 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[submodule "jstips"] + url = https://github.com/loverajoel/jstips +[submodule "You-Dont-Need-jQuery"] + url = https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/description b/spec/fixtures/git/repo-with-submodules/git.git/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample new file mode 100755 index 000000000..a5d7b84a6 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample new file mode 100755 index 000000000..b58d1184a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample new file mode 100755 index 000000000..ec17ec193 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample new file mode 100755 index 000000000..4142082bc --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample new file mode 100755 index 000000000..68d62d544 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample new file mode 100755 index 000000000..6187dbf43 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample new file mode 100755 index 000000000..9773ed4cb --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample new file mode 100755 index 000000000..f093a02ec --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample new file mode 100755 index 000000000..d84758373 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/index b/spec/fixtures/git/repo-with-submodules/git.git/index new file mode 100644 index 0000000000000000000000000000000000000000..20f2cdc44cc7953336ba167ea622ac57807d3a5d GIT binary patch literal 377 zcmZ?q402{*U|<4bmax8AH9(pHMl%A%*d;cuU|?um!oa}z6(}VF#K#@JJfEqyBGBZ@ zN!AN~1>x3dVxRXgaO5bm(J>}Q<6<)jPt&P???hu`{ zb0-&rNMwGgu1kJiiLPI2YKm@FU}ebFyOM&fAm8sf7SNR@4UHPMTSp4?G3rl Zbbj)_S+51vScSe`S>5#c_E~B3?*J0haH#+Q literal 0 HcmV?d00001 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/info/exclude b/spec/fixtures/git/repo-with-submodules/git.git/info/exclude new file mode 100644 index 000000000..a5196d1be --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD new file mode 100644 index 000000000..d41c1a106 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d3e073baf592c56614c68ead9e2cd0a3880140cd joshaber 1452185922 -0500 commit (initial): first +d3e073baf592c56614c68ead9e2cd0a3880140cd d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 joshaber 1452186239 -0500 commit: submodules diff --git a/spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master b/spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master new file mode 100644 index 000000000..d41c1a106 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d3e073baf592c56614c68ead9e2cd0a3880140cd joshaber 1452185922 -0500 commit (initial): first +d3e073baf592c56614c68ead9e2cd0a3880140cd d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 joshaber 1452186239 -0500 commit: submodules diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD new file mode 100644 index 000000000..b7cdcfd53 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD @@ -0,0 +1 @@ +2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config new file mode 100644 index 000000000..6ce8d21dd --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config @@ -0,0 +1,14 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + worktree = ../../../You-Dont-Need-jQuery + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = https://github.com/oneuijs/You-Dont-Need-jQuery + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "master"] + remote = origin + merge = refs/heads/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir new file mode 100644 index 000000000..6b8710a71 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir @@ -0,0 +1 @@ +.git diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample new file mode 100755 index 000000000..a5d7b84a6 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample new file mode 100755 index 000000000..b58d1184a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample new file mode 100755 index 000000000..ec17ec193 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample new file mode 100755 index 000000000..4142082bc --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample new file mode 100755 index 000000000..68d62d544 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample new file mode 100755 index 000000000..6187dbf43 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample new file mode 100755 index 000000000..9773ed4cb --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample new file mode 100755 index 000000000..f093a02ec --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample new file mode 100755 index 000000000..d84758373 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/index b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/index new file mode 100644 index 0000000000000000000000000000000000000000..202c1f9ceb11e5e0d6143722d77602bbb12dd012 GIT binary patch literal 1919 zcmZ?q402{*U|<4b@vy#GH9(pHMl&)nu&_&P>}6nRT*AP>_!TH60>rA1+_iqB@0ulQ z%(ym3<(_T)&qdSEFmUK4B_^fj6eT0{qnXo(Y7V27+ppzEzdsPx6m;%ziaI~NlxwQS zHU>_;)Z(1Xyb_>6F#RwZ>Yjd7a~QOPr!}4ayY;<$z3q&JZe?3l&j%d4&A_FXo>`Ka zo|j*g3i1;Oz|4WsQ1d3BnKvu*yjbM|tI3QTUeC0Z`MopCWzV6j4BUDpMTupZ#d?*w zIS}K3<}G>xF&9EZ%sY1u)jSsC`-Y{Hy15_Uk=wJlB}psmN6W&1Vg_~}PiI%ZU{~Dk znuKbOMRBU4ifVL)z?6Awa&n~EYuS39Jo0AX337FG@paWrE!N9TfjA82KNt=5=VVm# z%nF51m2Nx!BH~3_`09OM_=S%Dx_Rmv+`P;bLgr0DHP1pp>&GOfwQuUIC-w{G{y6AZ z$GG^%L48Moa`obP7!!_6zpBxK%9RP*e9hqwlcJ`LQy zvS_tk@H1oErT>DWOu;c9EACfMr$NCB%o?fT69By7!hOV<8UVqL-HIH-2Pt$b{VVS)F z{hDQ>y$uy>zTdj?oPjqxu_!lDFF8LiO)sk$oY7$Zgwas{&Ox8-oDYyyW6y zz2bt@WU!ZE=D=vEd5h4@Q;hzjQ8%YeS3mZM?d;Dt7Ek$o=lMsdc`5n11kGEFY95Ev zx1EO>-rh@ow6%N+>(RYyUM)CleV;)H?7qU%)S^m+iFm^iScjq1W3t!!ifkfV_dnU& zs?F`#oy;k6tLPJh7}(s>lFS@n^#LqY;l5{J2nlip71!nrM#c(;T&wjK?Mt=eNxOY3 zH1TPxSk8+xre+|$45kVOT#lMsU! 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master new file mode 100644 index 000000000..a76a6d29f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD new file mode 100644 index 000000000..a76a6d29f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx new file mode 100644 index 0000000000000000000000000000000000000000..cb4b0b44e5f8cf06c01d8ac8368ec2008a8eb09a GIT binary patch literal 18152 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 literal 0 HcmV?d00001 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack new file mode 100644 index 0000000000000000000000000000000000000000..89a46ce6f10d13fb52dd189c2c05d6e9bd192155 GIT binary patch literal 202626 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