mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge pull request #15750 from atom/mb-remove-status-handler-task
Avoid process spawn overhead for refreshing git status
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
"fs-plus": "^3.0.1",
|
||||
"fstream": "0.1.24",
|
||||
"fuzzaldrin": "^2.1",
|
||||
"git-utils": "5.0.0",
|
||||
"git-utils": "5.1.0",
|
||||
"glob": "^7.1.1",
|
||||
"grim": "1.5.0",
|
||||
"jasmine-json": "~0.0",
|
||||
@@ -108,7 +108,7 @@
|
||||
"encoding-selector": "0.23.6",
|
||||
"exception-reporting": "0.41.4",
|
||||
"find-and-replace": "0.212.3",
|
||||
"fuzzy-finder": "1.6.0",
|
||||
"fuzzy-finder": "1.6.1",
|
||||
"github": "0.6.2",
|
||||
"git-diff": "1.3.6",
|
||||
"go-to-line": "0.32.1",
|
||||
|
||||
@@ -39,7 +39,7 @@ module.exports = function (packagedAppPath) {
|
||||
relativePath === path.join('..', 'node_modules', 'debug', 'node.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
temp = require('temp').track()
|
||||
{Directory} = require 'pathwatcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
GitRepositoryProvider = require '../src/git-repository-provider'
|
||||
|
||||
describe "GitRepositoryProvider", ->
|
||||
provider = null
|
||||
|
||||
beforeEach ->
|
||||
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
|
||||
|
||||
afterEach ->
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
describe "when specified a Directory with a Git repository", ->
|
||||
it "returns a Promise that resolves to a GitRepository", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.statusTask).toBeTruthy()
|
||||
expect(result.getType()).toBe 'git'
|
||||
|
||||
it "returns the same GitRepository for different Directory objects in the same repo", ->
|
||||
firstRepo = null
|
||||
secondRepo = null
|
||||
|
||||
waitsForPromise ->
|
||||
directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
provider.repositoryForDirectory(directory).then (result) -> firstRepo = result
|
||||
|
||||
waitsForPromise ->
|
||||
directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')
|
||||
provider.repositoryForDirectory(directory).then (result) -> secondRepo = result
|
||||
|
||||
runs ->
|
||||
expect(firstRepo).toBeInstanceOf GitRepository
|
||||
expect(firstRepo).toBe secondRepo
|
||||
|
||||
describe "when specified a Directory without a Git repository", ->
|
||||
it "returns a Promise that resolves to null", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory temp.mkdirSync('dir')
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBe null
|
||||
|
||||
describe "when specified a Directory with an invalid Git repository", ->
|
||||
it "returns a Promise that resolves to null", ->
|
||||
waitsForPromise ->
|
||||
dirPath = temp.mkdirSync('dir')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '')
|
||||
|
||||
directory = new Directory dirPath
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBe null
|
||||
|
||||
describe "when specified a Directory with a valid gitfile-linked repository", ->
|
||||
it "returns a Promise that resolves to a GitRepository", ->
|
||||
waitsForPromise ->
|
||||
gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
workDirPath = temp.mkdirSync('git-workdir')
|
||||
fs.writeFileSync(path.join(workDirPath, '.git'), 'gitdir: ' + gitDirPath+'\n')
|
||||
|
||||
directory = new Directory workDirPath
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.statusTask).toBeTruthy()
|
||||
expect(result.getType()).toBe 'git'
|
||||
|
||||
describe "when specified a Directory without existsSync()", ->
|
||||
directory = null
|
||||
provider = null
|
||||
beforeEach ->
|
||||
# An implementation of Directory that does not implement existsSync().
|
||||
subdirectory = {}
|
||||
directory =
|
||||
getSubdirectory: ->
|
||||
isRoot: -> true
|
||||
spyOn(directory, "getSubdirectory").andReturn(subdirectory)
|
||||
|
||||
it "returns null", ->
|
||||
repo = provider.repositoryForDirectorySync(directory)
|
||||
expect(repo).toBe null
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith(".git")
|
||||
|
||||
it "returns a Promise that resolves to null for the async implementation", ->
|
||||
waitsForPromise ->
|
||||
provider.repositoryForDirectory(directory).then (repo) ->
|
||||
expect(repo).toBe null
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith(".git")
|
||||
103
spec/git-repository-provider-spec.js
Normal file
103
spec/git-repository-provider-spec.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
const temp = require('temp').track()
|
||||
const {Directory} = require('pathwatcher')
|
||||
const GitRepository = require('../src/git-repository')
|
||||
const GitRepositoryProvider = require('../src/git-repository-provider')
|
||||
const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('GitRepositoryProvider', () => {
|
||||
let provider
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
|
||||
})
|
||||
|
||||
describe('.repositoryForDirectory(directory)', () => {
|
||||
describe('when specified a Directory with a Git repository', () => {
|
||||
it('resolves with a GitRepository', async () => {
|
||||
const directory = new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
const result = await provider.repositoryForDirectory(directory)
|
||||
expect(result).toBeInstanceOf(GitRepository)
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.getType()).toBe('git')
|
||||
|
||||
// Refresh should be started
|
||||
await new Promise(resolve => result.onDidChangeStatuses(resolve))
|
||||
})
|
||||
|
||||
it('resolves with the same GitRepository for different Directory objects in the same repo', async () => {
|
||||
const firstRepo = await provider.repositoryForDirectory(
|
||||
new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
)
|
||||
const secondRepo = await provider.repositoryForDirectory(
|
||||
new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
|
||||
)
|
||||
|
||||
expect(firstRepo).toBeInstanceOf(GitRepository)
|
||||
expect(firstRepo).toBe(secondRepo)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory without a Git repository', () => {
|
||||
it('resolves with null', async () => {
|
||||
const directory = new Directory(temp.mkdirSync('dir'))
|
||||
const repo = await provider.repositoryForDirectory(directory)
|
||||
expect(repo).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory with an invalid Git repository', () => {
|
||||
it('resolves with null', async () => {
|
||||
const dirPath = temp.mkdirSync('dir')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '')
|
||||
|
||||
const directory = new Directory(dirPath)
|
||||
const repo = await provider.repositoryForDirectory(directory)
|
||||
expect(repo).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory with a valid gitfile-linked repository', () => {
|
||||
it('returns a Promise that resolves to a GitRepository', async () => {
|
||||
const gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
const workDirPath = temp.mkdirSync('git-workdir')
|
||||
fs.writeFileSync(path.join(workDirPath, '.git'), `gitdir: ${gitDirPath}\n`)
|
||||
|
||||
const directory = new Directory(workDirPath)
|
||||
const result = await provider.repositoryForDirectory(directory)
|
||||
expect(result).toBeInstanceOf(GitRepository)
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.getType()).toBe('git')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory without existsSync()', () => {
|
||||
let directory
|
||||
|
||||
beforeEach(() => {
|
||||
// An implementation of Directory that does not implement existsSync().
|
||||
const subdirectory = {}
|
||||
directory = {
|
||||
getSubdirectory () {},
|
||||
isRoot () { return true }
|
||||
}
|
||||
spyOn(directory, 'getSubdirectory').andReturn(subdirectory)
|
||||
})
|
||||
|
||||
it('returns null', () => {
|
||||
const repo = provider.repositoryForDirectorySync(directory)
|
||||
expect(repo).toBe(null)
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith('.git')
|
||||
})
|
||||
|
||||
it('returns a Promise that resolves to null for the async implementation', async () => {
|
||||
const repo = await provider.repositoryForDirectory(directory)
|
||||
expect(repo).toBe(null)
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith('.git')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -283,11 +283,15 @@ describe "GitRepository", ->
|
||||
[editor] = []
|
||||
|
||||
beforeEach ->
|
||||
statusRefreshed = false
|
||||
atom.project.setPaths([copyRepository()])
|
||||
atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('other.txt').then (o) -> editor = o
|
||||
|
||||
waitsFor 'repo to refresh', -> statusRefreshed
|
||||
|
||||
it "emits a status-changed event when a buffer is saved", ->
|
||||
editor.insertNewline()
|
||||
|
||||
|
||||
@@ -1,496 +0,0 @@
|
||||
{join} = require 'path'
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
GitUtils = require 'git-utils'
|
||||
|
||||
Task = require './task'
|
||||
|
||||
# Extended: Represents the underlying git operations performed by Atom.
|
||||
#
|
||||
# This class shouldn't be instantiated directly but instead by accessing the
|
||||
# `atom.project` global and calling `getRepositories()`. Note that this will
|
||||
# only be available when the project is backed by a Git repository.
|
||||
#
|
||||
# This class handles submodules automatically by taking a `path` argument to many
|
||||
# of the methods. This `path` argument will determine which underlying
|
||||
# repository is used.
|
||||
#
|
||||
# For a repository with submodules this would have the following outcome:
|
||||
#
|
||||
# ```coffee
|
||||
# repo = atom.project.getRepositories()[0]
|
||||
# repo.getShortHead() # 'master'
|
||||
# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
|
||||
# ```
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ### Logging the URL of the origin remote
|
||||
#
|
||||
# ```coffee
|
||||
# git = atom.project.getRepositories()[0]
|
||||
# console.log git.getOriginURL()
|
||||
# ```
|
||||
#
|
||||
# ### Requiring in packages
|
||||
#
|
||||
# ```coffee
|
||||
# {GitRepository} = require 'atom'
|
||||
# ```
|
||||
module.exports =
|
||||
class GitRepository
|
||||
@exists: (path) ->
|
||||
if git = @open(path)
|
||||
git.destroy()
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
###
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
# Public: Creates a new GitRepository instance.
|
||||
#
|
||||
# * `path` The {String} path to the Git 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 {GitRepository} instance or `null` if the repository could not be opened.
|
||||
@open: (path, options) ->
|
||||
return null unless path
|
||||
try
|
||||
new GitRepository(path, options)
|
||||
catch
|
||||
null
|
||||
|
||||
constructor: (path, options={}) ->
|
||||
@emitter = new Emitter
|
||||
@subscriptions = new CompositeDisposable
|
||||
|
||||
@repo = GitUtils.open(path)
|
||||
unless @repo?
|
||||
throw new Error("No Git repository found searching path: #{path}")
|
||||
|
||||
@statuses = {}
|
||||
@upstream = {ahead: 0, behind: 0}
|
||||
for submodulePath, submoduleRepo of @repo.submodules
|
||||
submoduleRepo.upstream = {ahead: 0, behind: 0}
|
||||
|
||||
{@project, @config, 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 {GitRepository} object.
|
||||
#
|
||||
# This destroys any tasks and subscriptions and releases the underlying
|
||||
# libgit2 repository handle. This method is idempotent.
|
||||
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
|
||||
|
||||
# Public: Returns a {Boolean} indicating if this repository has been destroyed.
|
||||
isDestroyed: ->
|
||||
not @repo?
|
||||
|
||||
# Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
# is invoked.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# 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 `"git"`.
|
||||
getType: -> 'git'
|
||||
|
||||
# Public: Returns the {String} path of the repository.
|
||||
getPath: ->
|
||||
@path ?= fs.absolute(@getRepo().getPath())
|
||||
|
||||
# Public: Returns the {String} working directory path of the repository.
|
||||
getWorkingDirectory: -> @getRepo().getWorkingDirectory()
|
||||
|
||||
# Public: Returns true if at the root, false if in a subfolder of the
|
||||
# repository.
|
||||
isProjectAtRoot: ->
|
||||
@projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is ''
|
||||
|
||||
# Public: Makes a path relative to the repository's working directory.
|
||||
relativize: (path) -> @getRepo().relativize(path)
|
||||
|
||||
# Public: Returns true if the given branch exists.
|
||||
hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")?
|
||||
|
||||
# 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) -> @getRepo(path).getShortHead()
|
||||
|
||||
# Public: Is the given path a submodule in the repository?
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isSubmodule: (path) ->
|
||||
return false unless path
|
||||
|
||||
repo = @getRepo(path)
|
||||
if repo.isSubmodule(repo.relativize(path))
|
||||
true
|
||||
else
|
||||
# Check if the path is a working directory in a repo that isn't the root.
|
||||
repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir'
|
||||
|
||||
# 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) ->
|
||||
@getRepo(path).getAheadBehindCount(reference)
|
||||
|
||||
# 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) ->
|
||||
@getRepo(path).upstream ? @upstream
|
||||
|
||||
# Public: Returns the git configuration value specified by the key.
|
||||
#
|
||||
# * `key` The {String} key for the configuration to lookup.
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key)
|
||||
|
||||
# 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) -> @getConfigValue('remote.origin.url', path)
|
||||
|
||||
# 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) -> @getRepo(path).getUpstreamBranch()
|
||||
|
||||
# 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) -> @getRepo(path).getReferences()
|
||||
|
||||
# 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) ->
|
||||
@getRepo(path).getReferenceTarget(reference)
|
||||
|
||||
###
|
||||
Section: Reading Status
|
||||
###
|
||||
|
||||
# Public: Returns true if the given path is modified.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is modified.
|
||||
isPathModified: (path) -> @isStatusModified(@getPathStatus(path))
|
||||
|
||||
# Public: Returns true if the given path is new.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is new.
|
||||
isPathNew: (path) -> @isStatusNew(@getPathStatus(path))
|
||||
|
||||
# Public: Is the given path ignored?
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is ignored.
|
||||
isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path))
|
||||
|
||||
# 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) ->
|
||||
directoryPath = "#{@relativize(directoryPath)}/"
|
||||
directoryStatus = 0
|
||||
for statusPath, status of @statuses
|
||||
directoryStatus |= status if statusPath.indexOf(directoryPath) is 0
|
||||
directoryStatus
|
||||
|
||||
# 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 = @getRepo(path)
|
||||
relativePath = @relativize(path)
|
||||
currentPathStatus = @statuses[relativePath] ? 0
|
||||
pathStatus = repo.getStatus(repo.relativize(path)) ? 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}
|
||||
|
||||
pathStatus
|
||||
|
||||
# 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) ->
|
||||
@statuses[@relativize(path)]
|
||||
|
||||
# Public: Returns true if the given status indicates modification.
|
||||
#
|
||||
# * `status` A {Number} representing the status.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `status` indicates modification.
|
||||
isStatusModified: (status) -> @getRepo().isStatusModified(status)
|
||||
|
||||
# Public: Returns true if the given status indicates a new path.
|
||||
#
|
||||
# * `status` A {Number} representing the status.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `status` indicates a new path.
|
||||
isStatusNew: (status) -> @getRepo().isStatusNew(status)
|
||||
|
||||
###
|
||||
Section: 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 an {Object} with the following keys:
|
||||
# * `added` The {Number} of added lines.
|
||||
# * `deleted` The {Number} of deleted lines.
|
||||
getDiffStats: (path) ->
|
||||
repo = @getRepo(path)
|
||||
repo.getDiffStats(repo.relativize(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) ->
|
||||
# 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)
|
||||
|
||||
###
|
||||
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 -- <path>
|
||||
# git checkout HEAD -- <path>
|
||||
# ```
|
||||
#
|
||||
# * `path` The {String} path to checkout.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the method was successful.
|
||||
checkoutHead: (path) ->
|
||||
repo = @getRepo(path)
|
||||
headCheckedOut = repo.checkoutHead(repo.relativize(path))
|
||||
@getPathStatus(path) if headCheckedOut
|
||||
headCheckedOut
|
||||
|
||||
# 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) ->
|
||||
@getRepo().checkoutReference(reference, create)
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
# Subscribes to buffer events.
|
||||
subscribeToBuffer: (buffer) ->
|
||||
getBufferPathStatus = =>
|
||||
if bufferPath = buffer.getPath()
|
||||
@getPathStatus(bufferPath)
|
||||
|
||||
getBufferPathStatus()
|
||||
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) ->
|
||||
buffer = editor.getBuffer()
|
||||
if filePath = buffer.getPath()
|
||||
@checkoutHead(filePath)
|
||||
buffer.reload()
|
||||
|
||||
# Returns the corresponding {Repository}
|
||||
getRepo: (path) ->
|
||||
if @repo?
|
||||
@repo.submoduleForPath(path) ? @repo
|
||||
else
|
||||
throw new Error("Repository has been destroyed")
|
||||
|
||||
# Reread the index to update any values that have changed since the
|
||||
# last time the index was read.
|
||||
refreshIndex: -> @getRepo().refreshIndex()
|
||||
|
||||
# Refreshes the current git status in an outside process and asynchronously
|
||||
# updates the relevant properties.
|
||||
refreshStatus: ->
|
||||
@handlerPath ?= require.resolve('./repository-status-handler')
|
||||
|
||||
relativeProjectPaths = @project?.getPaths()
|
||||
.map (projectPath) => @relativize(projectPath)
|
||||
.filter (projectPath) -> projectPath.length > 0 and not path.isAbsolute(projectPath)
|
||||
|
||||
@statusTask?.terminate()
|
||||
new Promise (resolve) =>
|
||||
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({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
|
||||
|
||||
for submodulePath, submoduleRepo of @getRepo().submodules
|
||||
submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0}
|
||||
|
||||
unless statusesUnchanged
|
||||
@emitter.emit 'did-change-statuses'
|
||||
resolve()
|
||||
603
src/git-repository.js
Normal file
603
src/git-repository.js
Normal file
@@ -0,0 +1,603 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS104: Avoid inline assignments
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const {join} = require('path')
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
const GitUtils = require('git-utils')
|
||||
|
||||
let nextId = 0
|
||||
|
||||
// Extended: Represents the underlying git operations performed by Atom.
|
||||
//
|
||||
// This class shouldn't be instantiated directly but instead by accessing the
|
||||
// `atom.project` global and calling `getRepositories()`. Note that this will
|
||||
// only be available when the project is backed by a Git repository.
|
||||
//
|
||||
// This class handles submodules automatically by taking a `path` argument to many
|
||||
// of the methods. This `path` argument will determine which underlying
|
||||
// repository is used.
|
||||
//
|
||||
// For a repository with submodules this would have the following outcome:
|
||||
//
|
||||
// ```coffee
|
||||
// repo = atom.project.getRepositories()[0]
|
||||
// repo.getShortHead() # 'master'
|
||||
// repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
|
||||
// ```
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ### Logging the URL of the origin remote
|
||||
//
|
||||
// ```coffee
|
||||
// git = atom.project.getRepositories()[0]
|
||||
// console.log git.getOriginURL()
|
||||
// ```
|
||||
//
|
||||
// ### Requiring in packages
|
||||
//
|
||||
// ```coffee
|
||||
// {GitRepository} = require 'atom'
|
||||
// ```
|
||||
module.exports =
|
||||
class GitRepository {
|
||||
static exists (path) {
|
||||
const git = this.open(path)
|
||||
if (git) {
|
||||
git.destroy()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
// Public: Creates a new GitRepository instance.
|
||||
//
|
||||
// * `path` The {String} path to the Git 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 {GitRepository} instance or `null` if the repository could not be opened.
|
||||
static open (path, options) {
|
||||
if (!path) { return null }
|
||||
try {
|
||||
return new GitRepository(path, options)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
constructor (path, options = {}) {
|
||||
this.id = nextId++
|
||||
this.emitter = new Emitter()
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.repo = GitUtils.open(path)
|
||||
if (this.repo == null) {
|
||||
throw new Error(`No Git repository found searching path: ${path}`)
|
||||
}
|
||||
|
||||
this.statusRefreshCount = 0
|
||||
this.statuses = {}
|
||||
this.upstream = {ahead: 0, behind: 0}
|
||||
for (let submodulePath in this.repo.submodules) {
|
||||
const submoduleRepo = this.repo.submodules[submodulePath]
|
||||
submoduleRepo.upstream = {ahead: 0, behind: 0}
|
||||
}
|
||||
|
||||
this.project = options.project
|
||||
this.config = options.config
|
||||
|
||||
if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) {
|
||||
const onWindowFocus = () => {
|
||||
this.refreshIndex()
|
||||
this.refreshStatus()
|
||||
}
|
||||
|
||||
window.addEventListener('focus', onWindowFocus)
|
||||
this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus)))
|
||||
}
|
||||
|
||||
if (this.project != null) {
|
||||
this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer))
|
||||
this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer)))
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Destroy this {GitRepository} object.
|
||||
//
|
||||
// This destroys any tasks and subscriptions and releases the underlying
|
||||
// libgit2 repository handle. This method is idempotent.
|
||||
destroy () {
|
||||
this.repo = null
|
||||
|
||||
if (this.emitter) {
|
||||
this.emitter.emit('did-destroy')
|
||||
this.emitter.dispose()
|
||||
this.emitter = null
|
||||
}
|
||||
|
||||
if (this.subscriptions) {
|
||||
this.subscriptions.dispose()
|
||||
this.subscriptions = null
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Returns a {Boolean} indicating if this repository has been destroyed.
|
||||
isDestroyed () {
|
||||
return this.repo == null
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
// is invoked.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Repository Details
|
||||
*/
|
||||
|
||||
// Public: A {String} indicating the type of version control system used by
|
||||
// this repository.
|
||||
//
|
||||
// Returns `"git"`.
|
||||
getType () { return 'git' }
|
||||
|
||||
// Public: Returns the {String} path of the repository.
|
||||
getPath () {
|
||||
if (this.path == null) {
|
||||
this.path = fs.absolute(this.getRepo().getPath())
|
||||
}
|
||||
return this.path
|
||||
}
|
||||
|
||||
// Public: Returns the {String} working directory path of the repository.
|
||||
getWorkingDirectory () {
|
||||
return this.getRepo().getWorkingDirectory()
|
||||
}
|
||||
|
||||
// Public: Returns true if at the root, false if in a subfolder of the
|
||||
// repository.
|
||||
isProjectAtRoot () {
|
||||
if (this.projectAtRoot == null) {
|
||||
this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) === ''
|
||||
}
|
||||
return this.projectAtRoot
|
||||
}
|
||||
|
||||
// Public: Makes a path relative to the repository's working directory.
|
||||
relativize (path) {
|
||||
return this.getRepo().relativize(path)
|
||||
}
|
||||
|
||||
// Public: Returns true if the given branch exists.
|
||||
hasBranch (branch) {
|
||||
return this.getReferenceTarget(`refs/heads/${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 this.getRepo(path).getShortHead()
|
||||
}
|
||||
|
||||
// Public: Is the given path a submodule in the repository?
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isSubmodule (path) {
|
||||
if (!path) return false
|
||||
|
||||
const repo = this.getRepo(path)
|
||||
if (repo.isSubmodule(repo.relativize(path))) {
|
||||
return true
|
||||
} else {
|
||||
// Check if the path is a working directory in a repo that isn't the root.
|
||||
return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir'
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getRepo(path).getAheadBehindCount(reference)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getRepo(path).upstream || this.upstream
|
||||
}
|
||||
|
||||
// Public: Returns the git configuration value specified by the key.
|
||||
//
|
||||
// * `key` The {String} key for the configuration to lookup.
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
getConfigValue (key, path) {
|
||||
return this.getRepo(path).getConfigValue(key)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getConfigValue('remote.origin.url', path)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getRepo(path).getUpstreamBranch()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getRepo(path).getReferences()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getRepo(path).getReferenceTarget(reference)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Reading Status
|
||||
*/
|
||||
|
||||
// Public: Returns true if the given path is modified.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is modified.
|
||||
isPathModified (path) {
|
||||
return this.isStatusModified(this.getPathStatus(path))
|
||||
}
|
||||
|
||||
// Public: Returns true if the given path is new.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is new.
|
||||
isPathNew (path) {
|
||||
return this.isStatusNew(this.getPathStatus(path))
|
||||
}
|
||||
|
||||
// Public: Is the given path ignored?
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is ignored.
|
||||
isPathIgnored (path) {
|
||||
return this.getRepo().isIgnored(this.relativize(path))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
directoryPath = `${this.relativize(directoryPath)}/`
|
||||
let directoryStatus = 0
|
||||
for (let statusPath in this.statuses) {
|
||||
const status = this.statuses[statusPath]
|
||||
if (statusPath.startsWith(directoryPath)) directoryStatus |= status
|
||||
}
|
||||
return directoryStatus
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const repo = this.getRepo(path)
|
||||
const relativePath = this.relativize(path)
|
||||
const currentPathStatus = this.statuses[relativePath] || 0
|
||||
let pathStatus = repo.getStatus(repo.relativize(path)) || 0
|
||||
if (repo.isStatusIgnored(pathStatus)) pathStatus = 0
|
||||
if (pathStatus > 0) {
|
||||
this.statuses[relativePath] = pathStatus
|
||||
} else {
|
||||
delete this.statuses[relativePath]
|
||||
}
|
||||
if (currentPathStatus !== pathStatus) {
|
||||
this.emitter.emit('did-change-status', {path, pathStatus})
|
||||
}
|
||||
|
||||
return pathStatus
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.statuses[this.relativize(path)]
|
||||
}
|
||||
|
||||
// Public: Returns true if the given status indicates modification.
|
||||
//
|
||||
// * `status` A {Number} representing the status.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `status` indicates modification.
|
||||
isStatusModified (status) { return this.getRepo().isStatusModified(status) }
|
||||
|
||||
// Public: Returns true if the given status indicates a new path.
|
||||
//
|
||||
// * `status` A {Number} representing the status.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `status` indicates a new path.
|
||||
isStatusNew (status) {
|
||||
return this.getRepo().isStatusNew(status)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: 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 an {Object} with the following keys:
|
||||
// * `added` The {Number} of added lines.
|
||||
// * `deleted` The {Number} of deleted lines.
|
||||
getDiffStats (path) {
|
||||
const repo = this.getRepo(path)
|
||||
return repo.getDiffStats(repo.relativize(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) {
|
||||
// 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.
|
||||
const options = {ignoreEolWhitespace: process.platform === 'win32'}
|
||||
const repo = this.getRepo(path)
|
||||
return repo.getLineDiffs(repo.relativize(path), text, options)
|
||||
}
|
||||
|
||||
/*
|
||||
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 -- <path>
|
||||
// git checkout HEAD -- <path>
|
||||
// ```
|
||||
//
|
||||
// * `path` The {String} path to checkout.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the method was successful.
|
||||
checkoutHead (path) {
|
||||
const repo = this.getRepo(path)
|
||||
const headCheckedOut = repo.checkoutHead(repo.relativize(path))
|
||||
if (headCheckedOut) this.getPathStatus(path)
|
||||
return headCheckedOut
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return this.getRepo().checkoutReference(reference, create)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
// Subscribes to buffer events.
|
||||
subscribeToBuffer (buffer) {
|
||||
const getBufferPathStatus = () => {
|
||||
const bufferPath = buffer.getPath()
|
||||
if (bufferPath) this.getPathStatus(bufferPath)
|
||||
}
|
||||
|
||||
getBufferPathStatus()
|
||||
const bufferSubscriptions = new CompositeDisposable()
|
||||
bufferSubscriptions.add(buffer.onDidSave(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidReload(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidChangePath(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidDestroy(() => {
|
||||
bufferSubscriptions.dispose()
|
||||
return this.subscriptions.remove(bufferSubscriptions)
|
||||
}))
|
||||
this.subscriptions.add(bufferSubscriptions)
|
||||
}
|
||||
|
||||
// Subscribes to editor view event.
|
||||
checkoutHeadForEditor (editor) {
|
||||
const buffer = editor.getBuffer()
|
||||
const bufferPath = buffer.getPath()
|
||||
if (bufferPath) {
|
||||
this.checkoutHead(bufferPath)
|
||||
return buffer.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the corresponding {Repository}
|
||||
getRepo (path) {
|
||||
if (this.repo) {
|
||||
return this.repo.submoduleForPath(path) || this.repo
|
||||
} else {
|
||||
throw new Error('Repository has been destroyed')
|
||||
}
|
||||
}
|
||||
|
||||
// Reread the index to update any values that have changed since the
|
||||
// last time the index was read.
|
||||
refreshIndex () {
|
||||
return this.getRepo().refreshIndex()
|
||||
}
|
||||
|
||||
// Refreshes the current git status in an outside process and asynchronously
|
||||
// updates the relevant properties.
|
||||
async refreshStatus () {
|
||||
const statusRefreshCount = ++this.statusRefreshCount
|
||||
const repo = this.getRepo()
|
||||
|
||||
const relativeProjectPaths = this.project && this.project.getPaths()
|
||||
.map(projectPath => this.relativize(projectPath))
|
||||
.filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath))
|
||||
|
||||
const branch = await repo.getHeadAsync()
|
||||
const upstream = await repo.getAheadBehindCountAsync()
|
||||
|
||||
const statuses = {}
|
||||
const repoStatus = relativeProjectPaths.length > 0
|
||||
? await repo.getStatusAsync(relativeProjectPaths)
|
||||
: await repo.getStatusAsync()
|
||||
for (let filePath in repoStatus) {
|
||||
statuses[filePath] = repoStatus[filePath]
|
||||
}
|
||||
|
||||
const submodules = {}
|
||||
for (let submodulePath in repo.submodules) {
|
||||
const submoduleRepo = repo.submodules[submodulePath]
|
||||
submodules[submodulePath] = {
|
||||
branch: await submoduleRepo.getHeadAsync(),
|
||||
upstream: await submoduleRepo.getAheadBehindCountAsync()
|
||||
}
|
||||
|
||||
const workingDirectoryPath = submoduleRepo.getWorkingDirectory()
|
||||
const submoduleStatus = await submoduleRepo.getStatusAsync()
|
||||
for (let filePath in submoduleStatus) {
|
||||
const absolutePath = path.join(workingDirectoryPath, filePath)
|
||||
const relativizePath = repo.relativize(absolutePath)
|
||||
statuses[relativizePath] = submoduleStatus[filePath]
|
||||
}
|
||||
}
|
||||
|
||||
if (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return
|
||||
|
||||
const statusesUnchanged =
|
||||
_.isEqual(branch, this.branch) &&
|
||||
_.isEqual(statuses, this.statuses) &&
|
||||
_.isEqual(upstream, this.upstream) &&
|
||||
_.isEqual(submodules, this.submodules)
|
||||
|
||||
this.branch = branch
|
||||
this.statuses = statuses
|
||||
this.upstream = upstream
|
||||
this.submodules = submodules
|
||||
|
||||
for (let submodulePath in repo.submodules) {
|
||||
repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream
|
||||
}
|
||||
|
||||
if (!statusesUnchanged) this.emitter.emit('did-change-statuses')
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
Git = require 'git-utils'
|
||||
path = require 'path'
|
||||
|
||||
module.exports = (repoPath, paths = []) ->
|
||||
repo = Git.open(repoPath)
|
||||
|
||||
upstream = {}
|
||||
statuses = {}
|
||||
submodules = {}
|
||||
branch = null
|
||||
|
||||
if repo?
|
||||
# Statuses in main repo
|
||||
workingDirectoryPath = repo.getWorkingDirectory()
|
||||
repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus())
|
||||
for filePath, status of repoStatus
|
||||
statuses[filePath] = status
|
||||
|
||||
# Statuses in submodules
|
||||
for submodulePath, submoduleRepo of repo.submodules
|
||||
submodules[submodulePath] =
|
||||
branch: submoduleRepo.getHead()
|
||||
upstream: submoduleRepo.getAheadBehindCount()
|
||||
|
||||
workingDirectoryPath = submoduleRepo.getWorkingDirectory()
|
||||
for filePath, status of submoduleRepo.getStatus()
|
||||
absolutePath = path.join(workingDirectoryPath, filePath)
|
||||
# Make path relative to parent repository
|
||||
relativePath = repo.relativize(absolutePath)
|
||||
statuses[relativePath] = status
|
||||
|
||||
upstream = repo.getAheadBehindCount()
|
||||
branch = repo.getHead()
|
||||
repo.release()
|
||||
|
||||
{statuses, upstream, branch, submodules}
|
||||
Reference in New Issue
Block a user