Set up the atom.repository-provider service and implement GitRepositoryProvider.

I tested this by creating a dummy implementation of an `HgRepositoryProvider`
(with the optional `createRepositorySync()` method implemented) and an `HgRepository`
in an Atom package with the following stanza in the `package.json`:

```
  "providedServices": {
    "atom.repository-provider": {
      "versions": {
        "0.1.0": "createHgRepositoryProvider"
      }
    }
  },
```

I opened a path with an Hg repository from the command line using Atom.
I verified that `atom.project.repositoryProviders` contains both a
`GitRepositoryProvider` and an `HgRepositoryProvider`.

I also verified that running the following printed out an `HgRepository`:

```
var Directory = require('pathwatcher').Directory;
atom.project.repositoryForDirectory(
    new Directory(atom.project.getPath(), /* symlink */ false)).then(
        function(repo) { console.log('repo: %o', repo); });
```

One thing that stands out to me about the current API for the
atom.repository-provider service is that the function used to create
a `RepositoryProvider` does not receive any arguments. If the creation
of the `GitRepositoryProvider` were done via the service, this would
be a problem because it needs a reference to `atom.project`, which is
not defined when it is created. (We work around this because it is
created in `Project`'s constructor, so it can pass `this` to
`new GitRepositoryProvider()`.)

We would have to create a `RepositoryProviderFactory` or something if
we wanted to specify arguments when creating a `RepositoryProvider`,
in general. Maybe that's too crazy / not an issue, in practice.

Though note that `GitRepository` cannot access `atom.project` lazily
because it uses it in its constructor to do the following:

```
if @project?
  @subscriptions.add @project.eachBuffer (buffer) => @subscribeToBuffer(buffer)
```

So long as we can guarantee that `atom.project` is defined before the
other providers are initialized, I think we should be OK.

Follow-up work:
* Replace the use of `RepositoryProvider.createRepositorySync()` with
`RepositoryProvider.repositoryForDirectory()` in `Project.setPaths()`.
* Replace all uses of `Project.getRepositories()` with
`Project.repositoryForDirectory()` in packages that are bundled with Atom
by default.
* Implement `Directory.exists()` and/or `Directory.existsSync()` and update
`git-repositor-provider.coffee`, as appropriate.
* Eliminate `GitRepositoryProvider.repositoryForDirectory()`'s use of
synchronous methods.
* Somewhat orthogonal to this diff, but the following fields need to be
removed from `Project` because they enforce the idea of a single root:
`path`, `rootDirectory`, and `repo`. This has implications around the
existing use of `@rootDirectory?.off()` and `@destroyRepo()`.
This commit is contained in:
Michael Bolin
2015-02-10 17:43:59 -05:00
parent 04d015a7e0
commit e04f17fe5f
3 changed files with 150 additions and 6 deletions

View File

@@ -0,0 +1,80 @@
fs = require 'fs'
GitRepository = require './git-repository'
# Checks whether the specified directory has, or has a parent directory that
# has, a valid .git folder, indicating the root of a Git repository.
# If found, 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.getRealPathSync())
# 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(@createRepositorySync(directory))
# Returns either:
# * {GitRepository} if the given directory has a Git repository.
# * `null` if the given directory does not have a Git repository.
createRepositorySync: (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.getRealPathSync()
repo = @pathToRepository[gitDirPath]
unless repo
repo = GitRepository.open(gitDirPath, project: @project)
repo.onDestroy(() => delete @pathToRepository[gitDirPath])
@pathToRepository[gitDirPath] = repo
repo.refreshIndex()
repo.refreshStatus()
repo

View File

@@ -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.off()
@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.
onDestroy: (callback) ->
@emitter.on 'did-destroy', callback
###
Section: Event Subscription

View File

@@ -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.
@directoryToRepositoryMap = 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,45 @@ 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:
# ```
# project.getDirectories().map(
# project.repositoryForDirectory.bind(project))
# ```
getRepositories: -> _.compact([@repo])
getRepo: ->
Grim.deprecate("Use ::getRepositories instead")
@repo
# Public: 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.
#
# * `directory` {Directory} for which to get a {Repository}.
repositoryForDirectory: (directory) ->
path = directory.getRealPathSync()
promise = @directoryToRepositoryMap.get(path)
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
# @directoryToRepositoryMap in case some other RepositoryProvider is
# registered in the future that could supply a Repository for the
# directory.
if repo is null
@directoryToRepositoryMap.delete path
repo)
@directoryToRepositoryMap.set(path, promise)
promise
###
Section: Managing Paths
###
@@ -126,9 +174,13 @@ 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
if provider.createRepositorySync
@repo = provider.createRepositorySync @rootDirectory
if @repo
break
else
@rootDirectory = null