/
githubmirror
/
atom
Обзор
Документация
Войти
/
githubmirror
/
atom
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/git-repository.js
621 строка
19 KB
Rafael Oleza
Reformat all JS files using prettier
31 май 2019, 19:33
31 май 2019, 19:33
7f3f040
Код
Авторство
О чём код?
const path = require('path'); const fs = require('fs-plus'); const _ = require('underscore-plus'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); 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} 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(filePath) { if (!filePath) return false; const repo = this.getRepo(filePath); if (repo.isSubmodule(repo.relativize(filePath))) { return true; } else { // Check if the filePath is a working directory in a repo that isn't the root. return ( repo !== this.getRepo() && repo.relativize(path.join(filePath, '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'); } };