Files
atom/src/git-repository-async.js
Daniel Hengeveld 806047bd27 Add missing return
2015-10-26 16:38:09 +01:00

309 lines
9.1 KiB
JavaScript

'use babel'
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
// 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, options = {}) {
// QUESTION: Should this wrap Git.Repository and reject with a nicer message?
return new GitRepositoryAsync(path, options)
}
static get Git () {
return Git
}
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)
let {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 () {
this.subscriptions.dispose()
}
getPath () {
return this.repoPromise.then((repo) => {
return repo.path().replace(/\/$/, '')
})
}
isPathIgnored (_path) {
return this.repoPromise.then((repo) => {
return Git.Ignore.pathIsIgnored(repo, _path)
})
}
isPathModified (_path) {
return this._filterStatusesByPath(_path).then(function (statuses) {
return statuses.filter((status) => {
return status.isModified()
}).length > 0
})
}
isPathNew (_path) {
return this._filterStatusesByPath(_path).then(function (statuses) {
return statuses.filter((status) => {
return status.isNew()
}).length > 0
})
}
checkoutHead (_path) {
return this.repoPromise.then((repo) => {
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)
}).then(() => {
return this.getPathStatus(_path)
})
}
checkoutHeadForEditor (editor) {
return new Promise(function (resolve, reject) {
let filePath = editor.getPath()
if (filePath) {
if (editor.buffer.isModified()) {
editor.buffer.reload()
}
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
// one, otherwise 'current'.
getPathStatus (_path) {
let relativePath = this._gitUtilsRepo.relativize(_path)
return this.repoPromise.then((repo) => {
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.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.
//
// * `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 = 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) {
let directoryStatus = 0
let filteredBits = bits.filter(function (b) { return b > 0 })
if (filteredBits.length > 0) {
filteredBits.forEach(function (bit) {
directoryStatus |= bit
})
}
return directoryStatus
})
})
}
// 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)
})
}).then((newPathStatusCache) => {
if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) {
this.emitter.emit('did-change-statuses')
}
this.pathStatusCache = newPathStatusCache
return newPathStatusCache
})
}
// Section: Private
// ================
subscribeToBuffer (buffer) {
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
}
}
getCachedPathStatus (_path) {
return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)]
}
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
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())
})
})
}
_filterStatusesByDirectory (directoryPath) {
return this.repoPromise.then(function (repo) {
return repo.getStatus()
}).then(function (statuses) {
return statuses.filter((status) => {
return 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) => {
return 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(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
}
})
}
}