UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

574 lines (514 loc) 23.3 kB
Object.defineProperty(exports, '__esModule', { value: true }); /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })(); var _createDecoratedClass = (function () { function defineProperties(target, descriptors, initializers) { for (var i = 0; i < descriptors.length; i++) { var descriptor = descriptors[i]; var decorators = descriptor.decorators; var key = descriptor.key; delete descriptor.key; delete descriptor.decorators; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor || descriptor.initializer) descriptor.writable = true; if (decorators) { for (var f = 0; f < decorators.length; f++) { var decorator = decorators[f]; if (typeof decorator === 'function') { descriptor = decorator(target, key, descriptor) || descriptor; } else { throw new TypeError('The decorator for method ' + descriptor.key + ' is of the invalid type ' + typeof decorator); } } if (descriptor.initializer !== undefined) { initializers[key] = descriptor; continue; } } Object.defineProperty(target, key, descriptor); } } return function (Constructor, protoProps, staticProps, protoInitializers, staticInitializers) { if (protoProps) defineProperties(Constructor.prototype, protoProps, protoInitializers); if (staticProps) defineProperties(Constructor, staticProps, staticInitializers); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _atom2; function _atom() { return _atom2 = require('atom'); } var _constants2; function _constants() { return _constants2 = require('./constants'); } var _commonsNodeCollection2; function _commonsNodeCollection() { return _commonsNodeCollection2 = require('../../commons-node/collection'); } var _commonsNodeDebounce2; function _commonsNodeDebounce() { return _commonsNodeDebounce2 = _interopRequireDefault(require('../../commons-node/debounce')); } var _commonsNodePromise2; function _commonsNodePromise() { return _commonsNodePromise2 = require('../../commons-node/promise'); } var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _notifications2; function _notifications() { return _notifications2 = require('./notifications'); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _lruCache2; function _lruCache() { return _lruCache2 = _interopRequireDefault(require('lru-cache')); } var UPDATE_SELECTED_FILE_CHANGES_EVENT = 'update-selected-file-changes'; var UPDATE_DIRTY_FILES_EVENT = 'update-dirty-files'; var CHANGE_REVISIONS_EVENT = 'did-change-revisions'; var UPDATE_STATUS_DEBOUNCE_MS = 50; var FETCH_REV_INFO_RETRY_TIME_MS = 1000; var FETCH_REV_INFO_MAX_TRIES = 5; var diffStatusFetcher = undefined; function getDiffStatusFetcher() { if (diffStatusFetcher != null) { return diffStatusFetcher; } try { // $FlowFB diffStatusFetcher = require('./fb/services').diffStatusFetcher; } catch (e) { diffStatusFetcher = _asyncToGenerator(function* () { return new Map(); }); } return diffStatusFetcher; } // The revisions haven't changed if the revisions' ids are the same. // That's because commit ids are unique and incremental. // Also, any write operation will update them. // That way, we guarantee we only update the revisions state if the revisions are changed. function isEqualRevisionsStates(revisionsState1, revisionsState2) { if (revisionsState1 === revisionsState2) { return true; } if (revisionsState1 == null || revisionsState2 == null) { return false; } return (0, (_commonsNodeCollection2 || _commonsNodeCollection()).arrayEqual)(revisionsState1.revisions, revisionsState2.revisions, function (revision1, revision2) { return revision1.id === revision2.id && (0, (_commonsNodeCollection2 || _commonsNodeCollection()).arrayEqual)(revision1.bookmarks, revision2.bookmarks); }); } var RepositoryStack = (function () { function RepositoryStack(repository, diffOption) { var _this = this; _classCallCheck(this, RepositoryStack); this._repository = repository; this._emitter = new (_atom2 || _atom()).Emitter(); this._subscriptions = new (_atom2 || _atom()).CompositeDisposable(); this._dirtyFileChanges = new Map(); this._selectedFileChanges = new Map(); this._isActive = false; this._revisionIdToFileChanges = new (_lruCache2 || _lruCache()).default({ max: 100 }); this._fileContentsAtCommitIds = new (_lruCache2 || _lruCache()).default({ max: 20 }); this._selectedCompareCommitId = null; this._lastRevisionsState = null; this._commitIdsToDiffStatuses = new Map(); this._diffOption = diffOption; this._serializedUpdateStackState = (0, (_commonsNodePromise2 || _commonsNodePromise()).serializeAsyncCall)(function () { return _this._tryUpdateStackState(); }); this._serializedUpdateSelectedFileChanges = (0, (_commonsNodePromise2 || _commonsNodePromise()).serializeAsyncCall)(function () { return _this._updateSelectedFileChanges(); }); this._serializedUpdateDiffStatusForCommits = (0, (_commonsNodePromise2 || _commonsNodePromise()).serializeAsyncCall)(function () { return _this._updateDiffStatusForCommits(); }); var debouncedSerializedUpdateStackState = (0, (_commonsNodeDebounce2 || _commonsNodeDebounce()).default)(this._serializedUpdateStackState, UPDATE_STATUS_DEBOUNCE_MS, false); this._serializedUpdateStackState(); // Get the initial project status, if it's not already there, // triggered by another integration, like the file tree. repository.getStatuses([repository.getProjectDirectory()]); this._subscriptions.add(repository.onDidChangeStatuses(function () { // Do the lightweight dirty cache update to reflect the changes, // While only commit merge changes consumers wait for its results. _this._updateDirtyFileChanges(); debouncedSerializedUpdateStackState(); })); } _createDecoratedClass(RepositoryStack, [{ key: 'setDiffOption', value: function setDiffOption(diffOption) { if (this._diffOption === diffOption) { return; } this._diffOption = diffOption; this._serializedUpdateSelectedFileChanges().catch((_notifications2 || _notifications()).notifyInternalError); } }, { key: 'activate', value: function activate() { if (this._isActive) { return; } this._isActive = true; this._serializedUpdateStackState(); } }, { key: 'deactivate', value: function deactivate() { this._isActive = false; this._fetchRevisionsPromise = null; this._fileContentsAtCommitIds.reset(); } }, { key: '_tryUpdateStackState', decorators: [(0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackTiming)('diff-view.update-change-status')], value: _asyncToGenerator(function* () { if (!this._isActive) { return; } try { yield this._updateRevisionsState(); if (!this._isActive) { return; } yield this._serializedUpdateSelectedFileChanges(); } catch (error) { (0, (_notifications2 || _notifications()).notifyInternalError)(error); } }) }, { key: '_updateDirtyFileChanges', value: function _updateDirtyFileChanges() { this._dirtyFileChanges = this._getDirtyChangedStatus(); this._emitter.emit(UPDATE_DIRTY_FILES_EVENT); } }, { key: '_getDirtyChangedStatus', value: function _getDirtyChangedStatus() { var dirtyFileChanges = new Map(); var statuses = this._repository.getAllPathStatuses(); for (var filePath in statuses) { var changeStatus = (_constants2 || _constants()).HgStatusToFileChangeStatus[statuses[filePath]]; if (changeStatus != null) { dirtyFileChanges.set(filePath, changeStatus); } } return dirtyFileChanges; } }, { key: '_updateRevisionsState', value: _asyncToGenerator(function* () { // We should only update the revision state when the repository is active. if (!this._isActive) { return; } var lastRevisionsState = this._lastRevisionsState; var revisionsState = yield this.getRevisionsStatePromise(); this._lastRevisionsState = revisionsState; if (!isEqualRevisionsStates(revisionsState, lastRevisionsState)) { this._emitter.emit(CHANGE_REVISIONS_EVENT, revisionsState); this._serializedUpdateDiffStatusForCommits().catch(function (error) { (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)().warn('Failed to update diff status for commits', error); }); } }) }, { key: '_updateDiffStatusForCommits', value: _asyncToGenerator(function* () { if (!this._isActive) { return; } var cachedRevisionsState = yield this.getCachedRevisionsStatePromise(); this._commitIdsToDiffStatuses = yield getDiffStatusFetcher()(this._repository.getWorkingDirectory(), cachedRevisionsState.revisions); // Emit the new revisions state with the diff statuses. this._emitter.emit(CHANGE_REVISIONS_EVENT, (yield this.getCachedRevisionsStatePromise())); }) /** * Update the file change state comparing the dirty filesystem status * to a selected commit. * That would be a merge of `hg status` with the diff from commits, * and `hg log --rev ${revId}` for every commit. */ }, { key: '_updateSelectedFileChanges', value: _asyncToGenerator(function* () { var revisionsState = yield this.getCachedRevisionsStatePromise(); switch (this._diffOption) { case (_constants2 || _constants()).DiffOption.DIRTY: this._selectedFileChanges = this._dirtyFileChanges; break; case (_constants2 || _constants()).DiffOption.COMPARE_COMMIT: if (revisionsState.compareCommitId == null || revisionsState.compareCommitId === revisionsState.commitId) { this._selectedFileChanges = this._dirtyFileChanges; } else { // No need to fetch every commit file changes unless requested. yield this._updateSelectedChangesToCommit(revisionsState, revisionsState.compareCommitId); } break; case (_constants2 || _constants()).DiffOption.LAST_COMMIT: var revisions = revisionsState.revisions; if (revisions.length <= 1) { this._selectedFileChanges = this._dirtyFileChanges; } else { yield this._updateSelectedChangesToCommit(revisionsState, revisions[revisions.length - 2].id); } break; } this._emitter.emit(UPDATE_SELECTED_FILE_CHANGES_EVENT); }) }, { key: '_updateSelectedChangesToCommit', value: _asyncToGenerator(function* (revisionsState, beforeCommitId) { var latestToOldesRevisions = revisionsState.revisions.slice().reverse(); var revisionChanges = yield this._fetchFileChangesForRevisions(latestToOldesRevisions.filter(function (revision) { return revision.id > beforeCommitId; })); this._selectedFileChanges = this._mergeFileStatuses(this._dirtyFileChanges, revisionChanges); }) }, { key: 'getRevisionsStatePromise', value: _asyncToGenerator(function* () { var revisionPromise = this._fetchRevisionsPromise = this._fetchRevisions(); var revisions = undefined; try { revisions = yield this._fetchRevisionsPromise; } catch (error) { if (revisionPromise === this._fetchRevisionsPromise) { this._fetchRevisionsPromise = null; } throw error; } return this._createRevisionsState(revisions); }) }, { key: 'getCachedRevisionsStatePromise', value: _asyncToGenerator(function* () { if (this._fetchRevisionsPromise != null) { return this._createRevisionsState((yield this._fetchRevisionsPromise)); } else { return this.getRevisionsStatePromise(); } }) /** * Amend the revisions state with the latest selected valid compare commit id. */ }, { key: '_createRevisionsState', value: function _createRevisionsState(revisions) { var commitId = revisions[revisions.length - 1].id; // Prioritize the cached compaereCommitId, if it exists. // The user could have selected that from the timeline view. var compareCommitId = this._selectedCompareCommitId; if (!revisions.find(function (revision) { return revision.id === compareCommitId; })) { // Invalidate if there there is no longer a revision with that id. compareCommitId = null; } var diffStatuses = this._commitIdsToDiffStatuses; return { commitId: commitId, compareCommitId: compareCommitId, diffStatuses: diffStatuses, revisions: revisions }; } }, { key: '_fetchRevisions', decorators: [(0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackTiming)('diff-view.fetch-revisions-state')], value: _asyncToGenerator(function* () { var _this2 = this; if (!this._isActive) { throw new Error('Diff View should not fetch revisions while not active'); } // While rebasing, the common ancestor of `HEAD` and `BASE` // may be not applicable, but that's defined once the rebase is done. // Hence, we need to retry fetching the revision info (depending on the common ancestor) // because the watchman-based Mercurial updates doesn't consider or wait while rebasing. var revisions = yield (0, (_commonsNodePromise2 || _commonsNodePromise()).retryLimit)(function () { return _this2._repository.fetchRevisionInfoBetweenHeadAndBase(); }, function (result) { return result != null; }, FETCH_REV_INFO_MAX_TRIES, FETCH_REV_INFO_RETRY_TIME_MS); if (revisions == null || revisions.length === 0) { throw new Error('Cannot fetch revision info needed!'); } return revisions; }) }, { key: '_fetchFileChangesForRevisions', decorators: [(0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackTiming)('diff-view.fetch-revisions-change-history')], value: _asyncToGenerator(function* (revisions) { var _this3 = this; // Revision ids are unique and don't change, except when the revision is amended/rebased. // Hence, it's cached here to avoid service calls when working on a stack of commits. var revisionsFileHistory = yield Promise.all(revisions.map(_asyncToGenerator(function* (revision) { var id = revision.id; var changes = null; if (_this3._revisionIdToFileChanges.has(id)) { changes = _this3._revisionIdToFileChanges.get(id); } else { changes = yield _this3._repository.fetchFilesChangedAtRevision('' + id); if (changes == null) { throw new Error('Changes not available for revision: ' + id); } _this3._revisionIdToFileChanges.set(id, changes); } return changes; }))); return revisionsFileHistory; }) /** * Merges the file change statuses of the dirty filesystem state with * the revision changes, where dirty changes and more recent revisions * take priority in deciding which status a file is in. */ }, { key: '_mergeFileStatuses', value: function _mergeFileStatuses(dirtyStatus, revisionsFileChanges) { var mergedStatus = new Map(dirtyStatus); var mergedFilePaths = new Set(mergedStatus.keys()); function mergeStatusPaths(filePaths, changeStatusValue) { for (var filePath of filePaths) { if (!mergedFilePaths.has(filePath)) { mergedStatus.set(filePath, changeStatusValue); mergedFilePaths.add(filePath); } } } // More recent revision changes takes priority in specifying a files' statuses. var latestToOldestRevisionsChanges = revisionsFileChanges.slice().reverse(); for (var revisionFileChanges of latestToOldestRevisionsChanges) { var added = revisionFileChanges.added; var modified = revisionFileChanges.modified; var deleted = revisionFileChanges.deleted; mergeStatusPaths(added, (_constants2 || _constants()).FileChangeStatus.ADDED); mergeStatusPaths(modified, (_constants2 || _constants()).FileChangeStatus.MODIFIED); mergeStatusPaths(deleted, (_constants2 || _constants()).FileChangeStatus.REMOVED); } return mergedStatus; } }, { key: 'getDirtyFileChanges', value: function getDirtyFileChanges() { return this._dirtyFileChanges; } }, { key: 'getSelectedFileChanges', value: function getSelectedFileChanges() { return this._selectedFileChanges; } }, { key: 'fetchHgDiff', value: _asyncToGenerator(function* (filePath) { var revisionsState = yield this.getCachedRevisionsStatePromise(); var revisions = revisionsState.revisions; var commitId = revisionsState.commitId; // When `compareCommitId` is null, the `HEAD` commit contents is compared // to the filesystem, otherwise it compares that commit to filesystem. var compareCommitId = undefined; switch (this._diffOption) { case (_constants2 || _constants()).DiffOption.DIRTY: compareCommitId = commitId; break; case (_constants2 || _constants()).DiffOption.LAST_COMMIT: compareCommitId = revisions.length > 1 ? revisions[revisions.length - 2].id : commitId; break; case (_constants2 || _constants()).DiffOption.COMPARE_COMMIT: compareCommitId = revisionsState.compareCommitId || commitId; break; default: throw new Error('Invalid Diff Option: ' + this._diffOption); } var _revisions$filter = revisions.filter(function (revision) { return revision.id === compareCommitId; }); var _revisions$filter2 = _slicedToArray(_revisions$filter, 1); var revisionInfo = _revisions$filter2[0]; (0, (_assert2 || _assert()).default)(revisionInfo, 'Diff Viw Fetcher: revision with id ' + compareCommitId + ' not found'); if (!this._fileContentsAtCommitIds.has(compareCommitId)) { this._fileContentsAtCommitIds.set(compareCommitId, new Map()); } var fileContentsAtCommit = this._fileContentsAtCommitIds.get(compareCommitId); var committedContents = undefined; if (fileContentsAtCommit.has(filePath)) { committedContents = fileContentsAtCommit.get(filePath); (0, (_assert2 || _assert()).default)(committedContents != null); } else { committedContents = yield this._repository.fetchFileContentAtRevision(filePath, compareCommitId.toString()) // If the file didn't exist on the previous revision, return empty contents. .catch(function (_err) { return ''; }); fileContentsAtCommit.set(filePath, committedContents); } return { committedContents: committedContents, revisionInfo: revisionInfo }; }) }, { key: 'getTemplateCommitMessage', value: function getTemplateCommitMessage() { // TODO(t12228275) This is a stopgap hack, fix it. return this._repository.getTemplateCommitMessage(); } }, { key: 'setRevision', value: _asyncToGenerator(function* (revision) { var revisionsState = yield this.getCachedRevisionsStatePromise(); var revisions = revisionsState.revisions; (0, (_assert2 || _assert()).default)(revisions && revisions.find(function (check) { return check.id === revision.id; }), 'Diff Viw Timeline: non-applicable selected revision'); this._selectedCompareCommitId = revisionsState.compareCommitId = revision.id; this._emitter.emit(CHANGE_REVISIONS_EVENT, revisionsState); (0, (_assert2 || _assert()).default)(this._diffOption === (_constants2 || _constants()).DiffOption.COMPARE_COMMIT, 'Invalid Diff Option at setRevision time!'); yield this._serializedUpdateSelectedFileChanges().catch((_notifications2 || _notifications()).notifyInternalError); }) }, { key: 'onDidUpdateDirtyFileChanges', value: function onDidUpdateDirtyFileChanges(callback) { return this._emitter.on(UPDATE_DIRTY_FILES_EVENT, callback); } }, { key: 'onDidUpdateSelectedFileChanges', value: function onDidUpdateSelectedFileChanges(callback) { return this._emitter.on(UPDATE_SELECTED_FILE_CHANGES_EVENT, callback); } }, { key: 'onDidChangeRevisions', value: function onDidChangeRevisions(callback) { return this._emitter.on(CHANGE_REVISIONS_EVENT, callback); } }, { key: 'getRepository', value: function getRepository() { return this._repository; } }, { key: 'commit', value: function commit(message) { return this._repository.commit(message); } }, { key: 'amend', value: function amend(message) { return this._repository.amend(message); } }, { key: 'revert', value: function revert(filePaths) { return this._repository.revert(filePaths); } }, { key: 'addAll', value: function addAll(filePaths) { return this._repository.addAll(filePaths); } }, { key: 'dispose', value: function dispose() { this.deactivate(); this._subscriptions.dispose(); this._dirtyFileChanges.clear(); this._selectedFileChanges.clear(); this._revisionIdToFileChanges.reset(); } }]); return RepositoryStack; })(); exports.default = RepositoryStack; module.exports = exports.default;