mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge pull request #5491 from bolinfest/repository-provider
Set up the atom.repository-provider service and implement GitRepositoryP...
This commit is contained in:
42
spec/git-repository-provider-spec.coffee
Normal file
42
spec/git-repository-provider-spec.coffee
Normal file
@@ -0,0 +1,42 @@
|
||||
path = require 'path'
|
||||
{Directory} = require 'pathwatcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
GitRepositoryProvider = require '../src/git-repository-provider'
|
||||
|
||||
describe "GitRepositoryProvider", ->
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
|
||||
describe "when specified a Directory with a Git repository", ->
|
||||
it "returns a Promise that resolves to a GitRepository", ->
|
||||
waitsForPromise ->
|
||||
provider = new GitRepositoryProvider atom.project
|
||||
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()
|
||||
|
||||
it "returns the same GitRepository for different Directory objects in the same repo", ->
|
||||
provider = new GitRepositoryProvider atom.project
|
||||
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 ->
|
||||
provider = new GitRepositoryProvider atom.project
|
||||
directory = new Directory '/tmp'
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBe null
|
||||
@@ -5,6 +5,8 @@ _ = require 'underscore-plus'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
BufferedProcess = require '../src/buffered-process'
|
||||
{Directory} = require 'pathwatcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
|
||||
describe "Project", ->
|
||||
beforeEach ->
|
||||
@@ -197,6 +199,27 @@ describe "Project", ->
|
||||
atom.project.bufferForPath("b").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).not.toBe buffer
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
it "resolves to null when the directory does not have a repository", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory("/tmp")
|
||||
atom.project.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeNull()
|
||||
expect(atom.project.repositoryProviders.length).toBeGreaterThan 0
|
||||
expect(atom.project.repositoryPromisesByPath.size).toBe 0
|
||||
|
||||
it "resolves to a GitRepository and is cached when the given directory is a Git repo", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory(path.join(__dirname, '..'))
|
||||
promise = atom.project.repositoryForDirectory(directory)
|
||||
promise.then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
dirPath = directory.getRealPathSync()
|
||||
expect(result.getPath()).toBe path.join(dirPath, '.git')
|
||||
|
||||
# Verify that the result is cached.
|
||||
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
|
||||
|
||||
describe ".setPaths(path)", ->
|
||||
describe "when path is a file", ->
|
||||
it "sets its path to the files parent directory and updates the root directory", ->
|
||||
|
||||
79
src/git-repository-provider.coffee
Normal file
79
src/git-repository-provider.coffee
Normal file
@@ -0,0 +1,79 @@
|
||||
fs = require 'fs'
|
||||
GitRepository = require './git-repository'
|
||||
|
||||
# Checks whether a valid `.git` directory is contained within the given
|
||||
# directory or one of its ancestors. If so, a Directory that corresponds to the
|
||||
# `.git` folder will be returned. Otherwise, returns `null`.
|
||||
#
|
||||
# * `directory` {Directory} to explore whether it is part of a Git repository.
|
||||
findGitDirectorySync = (directory) ->
|
||||
# TODO: Fix node-pathwatcher/src/directory.coffee so the following methods
|
||||
# can return cached values rather than always returning new objects:
|
||||
# getParent(), getFile(), getSubdirectory().
|
||||
gitDir = directory.getSubdirectory('.git')
|
||||
if directoryExistsSync(gitDir) and isValidGitDirectorySync gitDir
|
||||
gitDir
|
||||
else if directory.isRoot()
|
||||
return null
|
||||
else
|
||||
findGitDirectorySync directory.getParent()
|
||||
|
||||
# Returns a boolean indicating whether the specified directory represents a Git
|
||||
# repository.
|
||||
#
|
||||
# * `directory` {Directory} whose base name is `.git`.
|
||||
isValidGitDirectorySync = (directory) ->
|
||||
# To decide whether a directory has a valid .git folder, we use
|
||||
# the heuristic adopted by the valid_repository_path() function defined in
|
||||
# node_modules/git-utils/deps/libgit2/src/repository.c.
|
||||
return directoryExistsSync(directory.getSubdirectory('objects')) and
|
||||
directory.getFile('HEAD').exists() and
|
||||
directoryExistsSync(directory.getSubdirectory('refs'))
|
||||
|
||||
# Returns a boolean indicating whether the specified directory exists.
|
||||
#
|
||||
# * `directory` {Directory} to check for existence.
|
||||
directoryExistsSync = (directory) ->
|
||||
# TODO: Directory should have its own existsSync() method. Currently, File has
|
||||
# an exists() method, which is synchronous, so it may be tricky to achieve
|
||||
# consistency between the File and Directory APIs. Once Directory has its own
|
||||
# method, this function should be replaced with direct calls to existsSync().
|
||||
return fs.existsSync(directory.getPath())
|
||||
|
||||
# Provider that conforms to the atom.repository-provider@0.1.0 service.
|
||||
module.exports =
|
||||
class GitRepositoryProvider
|
||||
|
||||
constructor: (@project) ->
|
||||
# Keys are real paths that end in `.git`.
|
||||
# Values are the corresponding GitRepository objects.
|
||||
@pathToRepository = {}
|
||||
|
||||
# Returns a {Promise} that resolves with either:
|
||||
# * {GitRepository} if the given directory has a Git repository.
|
||||
# * `null` if the given directory does not have a Git repository.
|
||||
repositoryForDirectory: (directory) ->
|
||||
# TODO: Currently, this method is designed to be async, but it relies on a
|
||||
# synchronous API. It should be rewritten to be truly async.
|
||||
Promise.resolve(@repositoryForDirectorySync(directory))
|
||||
|
||||
# Returns either:
|
||||
# * {GitRepository} if the given directory has a Git repository.
|
||||
# * `null` if the given directory does not have a Git repository.
|
||||
repositoryForDirectorySync: (directory) ->
|
||||
# Only one GitRepository should be created for each .git folder. Therefore,
|
||||
# we must check directory and its parent directories to find the nearest
|
||||
# .git folder.
|
||||
gitDir = findGitDirectorySync(directory)
|
||||
unless gitDir
|
||||
return null
|
||||
|
||||
gitDirPath = gitDir.getPath()
|
||||
repo = @pathToRepository[gitDirPath]
|
||||
unless repo
|
||||
repo = GitRepository.open(gitDirPath, project: @project)
|
||||
repo.onDidDestroy(() => delete @pathToRepository[gitDirPath])
|
||||
@pathToRepository[gitDirPath] = repo
|
||||
repo.refreshIndex()
|
||||
repo.refreshStatus()
|
||||
repo
|
||||
@@ -101,8 +101,13 @@ class GitRepository
|
||||
# Public: Destroy this {GitRepository} object.
|
||||
#
|
||||
# This destroys any tasks and subscriptions and releases the underlying
|
||||
# libgit2 repository handle.
|
||||
# libgit2 repository handle. This method is idempotent.
|
||||
destroy: ->
|
||||
if @emitter?
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
@emitter = null
|
||||
|
||||
if @statusTask?
|
||||
@statusTask.terminate()
|
||||
@statusTask = null
|
||||
@@ -111,7 +116,14 @@ class GitRepository
|
||||
@repo.release()
|
||||
@repo = null
|
||||
|
||||
@subscriptions.dispose()
|
||||
if @subscriptions?
|
||||
@subscriptions.dispose()
|
||||
@subscriptions = null
|
||||
|
||||
# Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
# is invoked.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
|
||||
@@ -15,7 +15,7 @@ Grim = require 'grim'
|
||||
|
||||
TextEditor = require './text-editor'
|
||||
Task = require './task'
|
||||
GitRepository = require './git-repository'
|
||||
GitRepositoryProvider = require './git-repository-provider'
|
||||
|
||||
# Extended: Represents a project that's opened in Atom.
|
||||
#
|
||||
@@ -39,6 +39,20 @@ class Project extends Model
|
||||
@emitter = new Emitter
|
||||
@buffers ?= []
|
||||
|
||||
# Mapping from the real path of a {Directory} to a {Promise} that resolves
|
||||
# to either a {Repository} or null. Ideally, the {Directory} would be used
|
||||
# as the key; however, there can be multiple {Directory} objects created for
|
||||
# the same real path, so it is not a good key.
|
||||
@repositoryPromisesByPath = new Map();
|
||||
|
||||
# Note that the GitRepositoryProvider is registered synchronously so that
|
||||
# it is available immediately on startup.
|
||||
@repositoryProviders = [new GitRepositoryProvider(this)]
|
||||
atom.packages.serviceHub.consume(
|
||||
'atom.repository-provider',
|
||||
'^0.1.0',
|
||||
(provider) => @repositoryProviders.push(provider))
|
||||
|
||||
@subscribeToBuffer(buffer) for buffer in @buffers
|
||||
|
||||
Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor") if path?
|
||||
@@ -96,11 +110,47 @@ class Project extends Model
|
||||
|
||||
# Public: Get an {Array} of {GitRepository}s associated with the project's
|
||||
# directories.
|
||||
#
|
||||
# This method will be removed in 2.0 because it does synchronous I/O.
|
||||
# Prefer the following, which evaluates to a {Promise} that resolves to an
|
||||
# {Array} of {Repository} objects:
|
||||
# ```
|
||||
# Promise.all(project.getDirectories().map(
|
||||
# project.repositoryForDirectory.bind(project)))
|
||||
# ```
|
||||
getRepositories: -> _.compact([@repo])
|
||||
getRepo: ->
|
||||
Grim.deprecate("Use ::getRepositories instead")
|
||||
@repo
|
||||
|
||||
# Public: Get the repository for a given directory asynchronously.
|
||||
#
|
||||
# * `directory` {Directory} for which to get a {Repository}.
|
||||
#
|
||||
# Returns a {Promise} that resolves with either:
|
||||
# * {Repository} if a repository can be created for the given directory
|
||||
# * `null` if no repository can be created for the given directory.
|
||||
repositoryForDirectory: (directory) ->
|
||||
pathForDirectory = directory.getRealPathSync()
|
||||
promise = @repositoryPromisesByPath.get(pathForDirectory)
|
||||
unless promise
|
||||
promises = @repositoryProviders.map (provider) ->
|
||||
provider.repositoryForDirectory directory
|
||||
promise = Promise.all(promises).then (repositories) =>
|
||||
# Find the first non-falsy value, if any.
|
||||
repos = repositories.filter((repo) -> repo)
|
||||
repo = repos[0] or null
|
||||
|
||||
# If no repository is found, remove the entry in for the directory in
|
||||
# @repositoryPromisesByPath in case some other RepositoryProvider is
|
||||
# registered in the future that could supply a Repository for the
|
||||
# directory.
|
||||
if repo is null
|
||||
@repositoryPromisesByPath.delete pathForDirectory
|
||||
repo
|
||||
@repositoryPromisesByPath.set(pathForDirectory, promise)
|
||||
promise
|
||||
|
||||
###
|
||||
Section: Managing Paths
|
||||
###
|
||||
@@ -126,9 +176,10 @@ class Project extends Model
|
||||
if projectPath?
|
||||
directory = if fs.isDirectorySync(projectPath) then projectPath else path.dirname(projectPath)
|
||||
@rootDirectory = new Directory(directory)
|
||||
if @repo = GitRepository.open(directory, project: this)
|
||||
@repo.refreshIndex()
|
||||
@repo.refreshStatus()
|
||||
|
||||
# For now, use only the repositoryProviders with a sync API.
|
||||
for provider in @repositoryProviders
|
||||
break if @repo = provider.repositoryForDirectorySync?(@rootDirectory)
|
||||
else
|
||||
@rootDirectory = null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user