mirror of
https://github.com/atom/atom.git
synced 2026-02-03 19:25:06 -05:00
309 lines
9.1 KiB
JavaScript
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
|
|
}
|
|
})
|
|
}
|
|
}
|