diff --git a/README.md b/README.md index d2a3ab0..8dc87ee 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Works on: - Steven Atkinson [@mrstebo](https://github.com/mrstebo) - [@mevatron](https://github.com/mevatron) - Tom Kemper [@TomKemperNL](https://github.com/TomKemperNL) +- Carlos Ortiz [@Goodwine](https://github.com/Goodwine) - Google Inc. ## License diff --git a/lib/hg-repository-async.coffee b/lib/hg-repository-async.coffee deleted file mode 100644 index c4c58e0..0000000 --- a/lib/hg-repository-async.coffee +++ /dev/null @@ -1,477 +0,0 @@ -{Emitter, Disposable, CompositeDisposable} = require 'event-kit' - -HgUtils = require './hg-utils' - -module.exports = -class HgRepositoryAsync - - # devMode: atom.inDevMode() - # workingDirectory: '' - - ### - Section: Construction and Destruction - ### - - # Public: Creates a new HgRepository instance. - # - # * `path` The {String} path to the Mercurial repository to open. - # * `options` An optional {Object} with the following keys: - # * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and - # statuses when the window is focused. - # - # Returns a {HgRepository} instance or `null` if the repository could not be opened. - @open: (path, options) -> - return null unless path - try - new HgRepositoryAsync(path, options) - catch - null - - constructor: (path, options={}) -> - @emitter = new Emitter - @subscriptions = new CompositeDisposable - - @repo = HgUtils.open(path) - unless @repo? - throw new Error("No Mercurial repository found searching path: #{path.path}") - - @symlink = HgUtils.resolveSymlink(path) - - @statuses = {} - @upstream = {ahead: 0, behind: 0} - - @cachedIgnoreStatuses = [] - @cachedHgFileContent = {} - - {@project, refreshOnWindowFocus} = options - - refreshOnWindowFocus ?= true - if refreshOnWindowFocus - onWindowFocus = => - @refreshIndex() - @refreshStatus() - - window.addEventListener 'focus', onWindowFocus - @subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus) - - if @project? - @project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer) - @subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer) - - # Public: Destroy this {HgRepository} object. - # - # This destroys any tasks and subscriptions and releases the HgRepository - # object - destroy: -> - if @emitter? - @emitter.emit 'did-destroy' - @emitter.dispose() - @emitter = null - - # if @statusTask? - # @statusTask.terminate() - # @statusTask = null - - if @repo? - # @repo.release() - @repo = null - - if @subscriptions? - @subscriptions.dispose() - @subscriptions = null - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when this HgRepository's destroy() method - # is invoked. - onDidDestroy: (callback) -> - @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) -> - @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) -> - @emitter.on 'did-change-statuses', callback - - ### - Section: Repository Details - ### - - # Public: A {String} indicating the type of version control system used by - # this repository. - # - # Returns `"hg"`. - getType: -> 'hg' - - # Public: Returns the {String} path of the repository. - _getPath: (repo) -> - @path ?= repo.getPath() - - getPath: -> - @getRepo.then(@_getPath.bind(this)) - - # Public: Sets the {String} working directory path of the repository. - setWorkingDirectory: (workingDirectory) -> - @workingDirectory = workingDirectory - - # Public: Returns the {String} working directory path of the repository. - getWorkingDirectory: -> - return @workingDirectory - - # Public: Returns true if at the root, false if in a subfolder of the - # repository. - isProjectAtRoot: -> - @projectAtRoot ?= @project?.relativize(@getPath()) is '' - - # Public: Makes a path relative to the repository's working directory. - relativize: (path) -> null - - # Slash win32 path - slashPath: (path) -> - return path unless path - if @symlink - path = path.replace(@path, @symlink) - - if path && path.indexOf('..') is 0 - path = path.replace('..', '') - - if path && path.indexOf('/private') is 0 - path = path.replace('/private', '') - - if process.platform is 'win32' - return path.replace(/\\/g, '/') - else - return path - - # Public: Returns true if the given branch exists. - hasBranch: (branch) -> null - - # 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) -> - return @getRepo(path).then((repo) -> - return repo.getShortHead()) - - # Public: Is the given path a submodule in the repository? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean}. - isSubmodule: (path) -> null - - # 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) -> null - - # 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) -> {ahead: 0, behind: 0} - - # Public: Returns the hg property 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) -> null - - # 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) -> null - - # 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) -> null - - # 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) -> null - - # 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) -> null - - ### - Section: Reading Status - ### - - # Public: Returns true if the given path is modified. - isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) - - # Public: Returns true if the given path is new. - isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) - - # Public: Is the given path ignored? - # - # Returns a {Boolean}. - # isPathIgnored: (path) -> @isStatusIgnored(@getPathStatus(path)) - _isPathIgnored: (path, resolve, reject) -> - resolve(@cachedIgnoreStatuses.indexOf(@slashPath(path)) != -1) - - isPathIgnored: (path) -> - return new Promise(@_isPathIgnored.bind(this, path)).then((response) -> - return response) - - # Public: Get the status of a directory in the repository's working directory. - # - # * `path` The {String} path to check. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - _getDirectoryStatus: (directoryPath, resolve, reject) -> - directoryPath = "#{@slashPath(directoryPath)}/" - directoryStatus = 0 - for path, status of @statuses - directoryStatus |= status if path.indexOf(directoryPath) is 0 - resolve(directoryStatus) - - getDirectoryStatus: (directoryPath) -> - return new Promise(@_getDirectoryStatus.bind(this, directoryPath)).then((status) -> - return status) - - # Public: Get the status of a single path in the repository. - # - # `path` A {String} repository-relative path. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - _getPathStatus: (path, repo) -> - relativePath = @slashPath(path) - currentPathStatus = @statuses[relativePath] ? 0 - pathStatus = repo.getPathStatus(relativePath) ? 0 - pathStatus = 0 if repo.isStatusIgnored(pathStatus) - if pathStatus > 0 - @statuses[relativePath] = pathStatus - else - delete @statuses[relativePath] - if currentPathStatus isnt pathStatus - @emitter.emit 'did-change-status', {path, pathStatus} - return pathStatus - - getPathStatus: (path) -> - return @getRepo().then(@_getPathStatus.bind(this, path)) - - # Public: Get the cached status for the given path. - # - # * `path` A {String} path in the repository, relative or absolute. - # - # Returns a status {Number} or null if the path is not in the cache. - _getCachedPathStatus: (path, resolve, reject) -> - return unless path - resolve(@statuses[@slashPath(path)]) - - getCachedPathStatus: (path) -> - return new Promise(@_getCachedPathStatus.bind(this, path)).then((status) -> - return status) - - # Public: Returns true if the given status indicates modification. - isStatusModified: (status) -> - return HgUtils.isStatusModified(status) - - # Public: Returns true if the given status indicates a new path. - isStatusNew: (status) -> - return HgUtils.isStatusNew(status) - - # Public: Returns true if the given status is ignored. - isStatusIgnored: (status) -> - return HgUtils.isStatusIgnored(status) - - # Public: Returns true if the given status is staged. - isStatusStaged: (status) -> - return HgUtils.isStatusStaged(status) - - ### - Section: Retrieving Diffs - ### - - # Retrieves the file content from latest hg revision and cache it. - # - # * `path` The {String} path for retrieving file contents. - # - # Returns a {String} with the filecontents - getCachedHgFileContent: (repo, path) -> - if (!@cachedHgFileContent[@slashPath(path)]) - @cachedHgFileContent[@slashPath(path)] = repo.getHgCat(path) - - return @cachedHgFileContent[@slashPath(path)] - - # 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 an {Object} with the following keys: - # * `added` The {Number} of added lines. - # * `deleted` The {Number} of deleted lines. - _getDiffStats: (path, repo) -> - return repo.getDiffStats(@slashPath(path), @getCachedHgFileContent(repo, path)) - - getDiffStats: (path) -> - return @getRepo().then(@_getDiffStats.bind(this, path)) - - # 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, repo) -> - # 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' - return repo.getLineDiffs(@getCachedHgFileContent(repo, path), text, options) - - getLineDiffs: (path, text) -> - return @getRepo().then(@_getLineDiffs.bind(this, path, text)) - - ### - Section: 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 {Boolean} that's true if the method was successful. - checkoutHead: (path) -> null - - # 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) -> null - - ### - Section: Private - ### - - # Subscribes to buffer events. - subscribeToBuffer: (buffer) -> - getBufferPathStatus = => - if path = buffer.getPath() - @getPathStatus(path) - - bufferSubscriptions = new CompositeDisposable - bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidDestroy => - bufferSubscriptions.dispose() - @subscriptions.remove(bufferSubscriptions) - @subscriptions.add(bufferSubscriptions) - return - - # Subscribes to editor view event. - checkoutHeadForEditor: (editor) -> null - - # Returns the corresponding {Repository} - _getRepo: (resolve, reject) -> - if @repo? - resolve(@repo) - else - reject(Error("Repository has been destroyed")) - - getRepo: () -> - return new Promise(@_getRepo.bind(this)).then((repo) -> - return repo) - - # Reread the index to update any values that have changed since the - # last time the index was read. - refreshIndex: -> null - - # Refreshes the current hg status in an outside process and asynchronously - # updates the relevant properties. - _refreshStatus: (repo) -> - @cachedIgnoreStatuses = repo.getRecursiveIgnoreStatuses() - - statusesDidChange = false - if repo.checkRepositoryHasChanged() - @statuses = {} - @cachedHgFileContent = {} - # cache recursiv ignore statuses - # @cachedIgnoreStatuses = @getRepo().getRecursiveIgnoreStatuses() - statusesDidChange = true - - for {status, path} in repo.getStatus() - slashedPath = @slashPath(path) - if @statuses[slashedPath] != status - @statuses[slashedPath] = status - statusesDidChange = true - - if statusesDidChange then @emitter.emit 'did-change-statuses' - - refreshStatus: -> - return @getRepo().then(@_refreshStatus.bind(this)) diff --git a/lib/hg-repository.coffee b/lib/hg-repository.coffee index 1546e36..ee84e28 100644 --- a/lib/hg-repository.coffee +++ b/lib/hg-repository.coffee @@ -1,7 +1,6 @@ {Emitter, Disposable, CompositeDisposable} = require 'event-kit' HgUtils = require './hg-utils' -HgRepositoryAsync = require './hg-repository-async' module.exports = class HgRepository @@ -39,9 +38,6 @@ class HgRepository @path = path @symlink = HgUtils.resolveSymlink(path) - # asyncOptions = _.clone(options) - @async = HgRepositoryAsync.open(path, options) - @statuses = {} @upstream = {ahead: 0, behind: 0} @@ -85,10 +81,6 @@ class HgRepository @subscriptions.dispose() @subscriptions = null - if @async? - @async.destroy() - @async = null - ### Section: Event Subscription ### @@ -328,11 +320,16 @@ class HgRepository # * `path` The {String} path for retrieving file contents. # # Returns a {String} with the filecontents - getCachedHgFileContent: (path) -> + getCachedHgFileContent: (path) => slashedPath = @slashPath(path) + + @repo.getHgCatAsync(path).then (contents) => + statusesDidChange = @cachedHgFileContent[slashedPath] != contents + @cachedHgFileContent[slashedPath] = contents + if statusesDidChange then @emitter.emit 'did-change-statuses' + if (!@cachedHgFileContent[slashedPath]) - repo = @getRepo() - @cachedHgFileContent[slashedPath] = repo.getHgCat(path) + return null return @cachedHgFileContent[slashedPath] # Public: Retrieves the number of lines added and removed to a path. @@ -434,17 +431,18 @@ class HgRepository @getRepo().getRecursiveIgnoreStatuses().then (allIgnored) => @cachedIgnoreStatuses = (@slashPath ignored for ignored in allIgnored) statusesDidChange = false - if @getRepo().checkRepositoryHasChanged() - @statuses = {} - @cachedHgFileContent = {} - # cache recursiv ignore statuses - # @cachedIgnoreStatuses = @getRepo().getRecursiveIgnoreStatuses() - statusesDidChange = true - - for {status, path} in @getRepo().getStatus() - slashedPath = @slashPath(path) - if @statuses[slashedPath] != status - @statuses[slashedPath] = status + @getRepo().checkRepositoryHasChangedAsync().then (hasChanged) => + if hasChanged + @statuses = {} + @cachedHgFileContent = {} + # cache recursiv ignore statuses + # @cachedIgnoreStatuses = @getRepo().getRecursiveIgnoreStatuses() statusesDidChange = true - - if statusesDidChange then @emitter.emit 'did-change-statuses' + @getRepo().getStatus().then (statuses) => + for {status, path} in statuses + slashedPath = @slashPath(path) + if @statuses[slashedPath] != status + @statuses[slashedPath] = status + statusesDidChange = true + + if statusesDidChange then @emitter.emit 'did-change-statuses' diff --git a/lib/hg-utils.coffee b/lib/hg-utils.coffee index effade4..79b5b59 100644 --- a/lib/hg-utils.coffee +++ b/lib/hg-utils.coffee @@ -81,26 +81,13 @@ class Repository # Parses info from `hg info` and `hgversion` command and checks if repo infos have changed # since last check # - # Returns a {boolean} if repo infos have changed - checkRepositoryHasChanged: () -> - hasChanged = false - revision = @getHgWorkingCopyRevision() - if revision? - if revision != @revision + # Returns a {Promise} of a {boolean} if repo infos have changed + checkRepositoryHasChangedAsync: () => + return @getHgWorkingCopyRevisionAsync().then (revision) => + if revision? and revision != @revision @revision = revision - hasChanged = true - - # info = @getHgInfo() - # if info? && info.url? - # if info.url != @url - # @url = info.url - # urlParts = urlParser.parse(info.url) - # @urlPath = urlParts.path - # pathParts = @urlPath.split('/') - # @shortHead = if pathParts.length > 0 then pathParts.pop() else '' - # hasChanged = true - - return hasChanged + return true + return false getShortHead: () -> branchFile = @rootPath + '/.hg/branch' @@ -123,10 +110,10 @@ class Repository # Parses `hg status`. Gets initially called by hg-repository.refreshStatus() # - # Returns a {Array} array keys are paths, values are change constants. Or null + # Returns a {Promise} of an {Array} array keys are paths, values are change + # constants. Or null getStatus: () -> - statuses = @getHgStatus() - return statuses + return @getHgStatusAsync() # Parses `hg status`. Gets called by hg-repository.refreshStatus() # @@ -262,7 +249,8 @@ class Repository if !util.isArray(params) params = [params] - return '' unless @isCommandForRepo(params) + if !@isCommandForRepo(params) + return '' child = spawnSync('hg', params, { cwd: @rootPath }) if child.status != 0 @@ -273,6 +261,7 @@ class Repository throw new Error(child.stdout.toString()) throw new Error('Error trying to execute Mercurial binary with params \'' + params + '\'') + return child.stdout.toString() hgCommandAsync: (params) -> @@ -281,7 +270,8 @@ class Repository if !util.isArray(params) params = [params] - return Promise.resolve('') unless @isCommandForRepo(params) + if !@isCommandForRepo(params) + return Promise.resolve('') flatArgs = params.reduce (prev, next) -> if next.indexOf? and next.indexOf(' ') != -1 @@ -291,7 +281,7 @@ class Repository , "" flatArgs = flatArgs.substring(1) - new Promise (resolve, reject) => + return new Promise (resolve, reject) => opts = cwd: @rootPath maxBuffer: 50 * 1024 * 1024 @@ -325,11 +315,9 @@ class Repository # Returns on success the current working copy revision. Otherwise null. # - # Returns a {String} with the current working copy revision - getHgWorkingCopyRevision: () -> - try - return @hgCommand(['id', '-i', @rootPath]) - catch error + # Returns a {Promise} of a {String} with the current working copy revision + getHgWorkingCopyRevisionAsync: () => + @hgCommandAsync(['id', '-i', @rootPath]).catch (error) => @handleHgError(error) return null @@ -350,32 +338,26 @@ class Repository @handleHgError error [] - # Returns on success an hg-status array. Otherwise null. - # Array keys are paths, values {Number} representing the status - # - # Returns a {Array} with path and statusnumber - getHgStatus: () -> - try - files = @hgCommand(['status', @rootPath]) - catch error + getHgStatusAsync: () -> + @hgCommandAsync(['status', @rootPath]).then (files) => + items = [] + entries = files.split('\n') + if entries + for entry in entries + parts = entry.split(' ') + status = parts[0] + pathPart = parts[1] + if pathPart? && status? + items.push({ + 'path': path.join @rootPath, pathPart + 'status': @mapHgStatus(status) + }) + + return items + .catch (error) => @handleHgError(error) return null - items = [] - entries = files.split('\n') - if entries - for entry in entries - parts = entry.split(' ') - status = parts[0] - pathPart = parts[1] - if pathPart? && status? - items.push({ - 'path': path.join @rootPath, pathPart - 'status': @mapHgStatus(status) - }) - - return items - # Returns on success a status bitmask. Otherwise null. # # * `hgPath` The path {String} for the status inquiry @@ -439,17 +421,14 @@ class Repository # # * `hgPath` The path {String} # - # Returns the {String} as filecontent - getHgCat: (hgPath) -> + # Returns {Promise} of a {String} with the filecontent + getHgCatAsync: (hgPath) -> params = ['cat', hgPath] - try - fileContent = @hgCommand(params) - return fileContent - catch error + return @hgCommandAsync(params).catch (error) => if /no such file in rev/.test(error) return null - @handleHgError(error) + @handleHgError error return null # This checks to see if the current params indicate whether we are working