diff --git a/services/web/Makefile b/services/web/Makefile
index 88d73461e4..a0f8c1c503 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -180,7 +180,7 @@ clean_css:
rm -f public/stylesheets/*.css*
clean_ci:
- docker-compose down
+ docker-compose down -v
test: test_unit test_frontend test_acceptance
@@ -221,7 +221,7 @@ test_acceptance_module: $(MODULE_MAKEFILES)
fi
test_clean:
- docker-compose ${DOCKER_COMPOSE_FLAGS} down
+ docker-compose ${DOCKER_COMPOSE_FLAGS} down -v
ci:
MOCHA_ARGS="--reporter tap" \
diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
index 0f49dc537c..8578f5b3f4 100644
--- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -57,26 +57,9 @@ module.exports = EditorHttpController =
privilegeLevel
)
- restoreDoc: (req, res, next) ->
- project_id = req.params.Project_id
- doc_id = req.params.doc_id
- name = req.body.name
-
- if !name?
- return res.sendStatus 400 # Malformed request
-
- logger.log project_id: project_id, doc_id: doc_id, "restoring doc"
- ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) =>
- return next(error) if error?
- EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc)
- res.json {
- doc_id: doc._id
- }
-
_nameIsAcceptableLength: (name)->
return name? and name.length < 150 and name.length != 0
-
addDoc: (req, res, next) ->
project_id = req.params.Project_id
name = req.body.name
diff --git a/services/web/app/coffee/Features/Editor/EditorRouter.coffee b/services/web/app/coffee/Features/Editor/EditorRouter.coffee
index 9de1544875..bcda59a16b 100644
--- a/services/web/app/coffee/Features/Editor/EditorRouter.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorRouter.coffee
@@ -14,8 +14,6 @@ module.exports =
webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc
webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder
- webRouter.post '/project/:Project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.restoreDoc
-
# Called by the real-time API to load up the current project state.
# This is a post request because it's more than just a getting of data. We take actions
# whenever a user joins a project, like updating the deleted status.
diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee
index e90aec9479..7d9d347531 100644
--- a/services/web/app/coffee/Features/History/HistoryController.coffee
+++ b/services/web/app/coffee/Features/History/HistoryController.coffee
@@ -6,6 +6,7 @@ Errors = require "../Errors/Errors"
HistoryManager = require "./HistoryManager"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
+RestoreManager = require "./RestoreManager"
module.exports = HistoryController =
selectHistoryApi: (req, res, next = (error) ->) ->
@@ -71,3 +72,29 @@ module.exports = HistoryController =
return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError
return next(error) if error?
res.sendStatus 204
+
+ restoreFileFromV2: (req, res, next) ->
+ {project_id} = req.params
+ {version, pathname} = req.body
+ user_id = AuthenticationController.getLoggedInUserId req
+ logger.log {project_id, version, pathname}, "restoring file from v2"
+ RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) ->
+ return next(error) if error?
+ res.json {
+ type: entity.type,
+ id: entity._id
+ }
+
+ restoreDocFromDeletedDoc: (req, res, next) ->
+ {project_id, doc_id} = req.params
+ {name} = req.body
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ if !name?
+ return res.sendStatus 400 # Malformed request
+ logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc"
+ RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) =>
+ return next(error) if error?
+ res.json {
+ doc_id: doc._id
+ }
+
diff --git a/services/web/app/coffee/Features/History/RestoreManager.coffee b/services/web/app/coffee/Features/History/RestoreManager.coffee
new file mode 100644
index 0000000000..ba7af44c5e
--- /dev/null
+++ b/services/web/app/coffee/Features/History/RestoreManager.coffee
@@ -0,0 +1,60 @@
+Settings = require 'settings-sharelatex'
+Path = require 'path'
+FileWriter = require '../../infrastructure/FileWriter'
+FileSystemImportManager = require '../Uploads/FileSystemImportManager'
+ProjectEntityHandler = require '../Project/ProjectEntityHandler'
+ProjectLocator = require '../Project/ProjectLocator'
+EditorController = require '../Editor/EditorController'
+Errors = require '../Errors/Errors'
+moment = require 'moment'
+
+module.exports = RestoreManager =
+ restoreDocFromDeletedDoc: (user_id, project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
+ # This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system.
+ # It looks up the deleted doc's contents, and then creates a new doc with the same content.
+ # We don't actually remove the deleted doc entry, just create a new one from its lines.
+ ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
+ return callback(error) if error?
+ addDocWithName = (name, callback) ->
+ EditorController.addDoc project_id, null, name, lines, 'restore', user_id, callback
+ RestoreManager._addEntityWithUniqueName addDocWithName, name, callback
+
+ restoreFileFromV2: (user_id, project_id, version, pathname, callback = (error, entity) ->) ->
+ RestoreManager._writeFileVersionToDisk project_id, version, pathname, (error, fsPath) ->
+ return callback(error) if error?
+ basename = Path.basename(pathname)
+ dirname = Path.dirname(pathname)
+ if dirname == '.' # no directory
+ dirname = ''
+ RestoreManager._findOrCreateFolder project_id, dirname, (error, parent_folder_id) ->
+ return callback(error) if error?
+ addEntityWithName = (name, callback) ->
+ FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, name, fsPath, false, callback
+ RestoreManager._addEntityWithUniqueName addEntityWithName, basename, callback
+
+ _findOrCreateFolder: (project_id, dirname, callback = (error, folder_id) ->) ->
+ EditorController.mkdirp project_id, dirname, (error, newFolders, lastFolder) ->
+ return callback(error) if error?
+ return callback(null, lastFolder?._id)
+
+ _addEntityWithUniqueName: (addEntityWithName, basename, callback = (error) ->) ->
+ addEntityWithName basename, (error, entity) ->
+ if error?
+ if error instanceof Errors.InvalidNameError
+ # likely a duplicate name, so try with a prefix
+ date = moment(new Date()).format('Do MMM YY H:mm:ss')
+ # Move extension to the end so the file type is preserved
+ extension = Path.extname(basename)
+ basename = Path.basename(basename, extension)
+ basename = "#{basename} (Restored on #{date})"
+ if extension != ''
+ basename = "#{basename}#{extension}"
+ addEntityWithName basename, callback
+ else
+ callback(error)
+ else
+ callback(null, entity)
+
+ _writeFileVersionToDisk: (project_id, version, pathname, callback = (error, fsPath) ->) ->
+ url = "#{Settings.apis.project_history.url}/project/#{project_id}/version/#{version}/#{encodeURIComponent(pathname)}"
+ FileWriter.writeUrlToDisk project_id, url, callback
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee
index e3aea07456..ac51e65957 100644
--- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee
@@ -121,15 +121,6 @@ module.exports = ProjectEntityUpdateHandler = self =
logger.log project_id: project_id, "removing root doc"
Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
- restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
- if not SafePath.isCleanFilename name
- return callback new Errors.InvalidNameError("invalid element name")
- # getDoc will return the deleted doc's lines, but we don't actually remove
- # the deleted doc, just create a new one from its lines.
- ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
- return callback(error) if error?
- self.addDoc project_id, null, name, lines, callback
-
addDoc: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
return callback(error) if error?
diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee
index b19dc83336..dedeed9bad 100644
--- a/services/web/app/coffee/infrastructure/FileWriter.coffee
+++ b/services/web/app/coffee/infrastructure/FileWriter.coffee
@@ -3,21 +3,40 @@ logger = require 'logger-sharelatex'
uuid = require 'uuid'
_ = require 'underscore'
Settings = require 'settings-sharelatex'
+request = require 'request'
-module.exports =
+module.exports = FileWriter =
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
callback = _.once(callback)
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
- writeStream = fs.createWriteStream(fsPath)
- stream.pipe(writeStream)
+ stream.pause()
+ fs.mkdir Settings.path.dumpFolder, (error) ->
+ stream.resume()
+ if error? and error.code != 'EEXIST'
+ # Ignore error about already existing
+ return callback(error)
- stream.on 'error', (err)->
- logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream"
- callback(err)
- writeStream.on 'error', (err)->
- logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk"
- callback(err)
- writeStream.on "finish", ->
- logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
- callback null, fsPath
\ No newline at end of file
+ writeStream = fs.createWriteStream(fsPath)
+ stream.pipe(writeStream)
+
+ stream.on 'error', (err)->
+ logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream"
+ callback(err)
+ writeStream.on 'error', (err)->
+ logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk"
+ callback(err)
+ writeStream.on "finish", ->
+ logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
+ callback null, fsPath
+
+ writeUrlToDisk: (identifier, url, callback = (error, fsPath) ->) ->
+ callback = _.once(callback)
+ stream = request.get(url)
+ stream.on 'response', (response) ->
+ if 200 <= response.statusCode < 300
+ FileWriter.writeStreamToDisk identifier, stream, callback
+ else
+ err = new Error("bad response from url: #{response.statusCode}")
+ logger.err {err, identifier, url}, err.message
+ callback(err)
\ No newline at end of file
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index 949967c7c3..7ec4dafbf4 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -201,8 +201,11 @@ module.exports = class Router
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
+ webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
+ webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
+
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
diff --git a/services/web/app/views/project/editor/history-file-tree.pug b/services/web/app/views/project/editor/history-file-tree.pug
index 9015dfe258..3356dc2249 100644
--- a/services/web/app/views/project/editor/history-file-tree.pug
+++ b/services/web/app/views/project/editor/history-file-tree.pug
@@ -11,7 +11,7 @@ aside.file-tree.file-tree-history(ng-controller="FileTreeController", ng-class="
.entity
.entity-name.entity-name-history(
ng-click="history.selection.pathname = pathname",
- ng-class="{ 'deleted': doc.deleted }"
+ ng-class="{ 'deleted': !!doc.deletedAtV }"
)
i.fa.fa-fw.fa-pencil
span {{ pathname }}
diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index 2fc6b02ae8..d366d5f41a 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -122,74 +122,8 @@ div#history(ng-show="ui.view == 'history'")
i.fa.fa-spin.fa-refresh
| #{translate("loading")}...
- .diff-panel.full-size(ng-controller="HistoryDiffController")
- .diff(
- ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary"
- )
- .toolbar.toolbar-alt
- span.name
- | {{history.diff.highlights.length}}
- ng-pluralize(
- count="history.diff.highlights.length",
- when="{\
- 'one': 'change',\
- 'other': 'changes'\
- }"
- )
- | in {{history.diff.pathname}}
- .toolbar-right
- a.btn.btn-danger.btn-sm(
- href,
- ng-if="!history.isV2"
- ng-click="openRestoreDiffModal()"
- ) #{translate("restore_to_before_these_changes")}
- .deleted-warning(
- ng-show="history.selection.docs[history.selection.pathname].deleted"
- ) This file was deleted
- .diff-editor.hide-ace-cursor(
- ace-editor="history",
- theme="settings.theme",
- font-size="settings.fontSize",
- text="history.diff.text",
- highlights="history.diff.highlights",
- read-only="true",
- resize-on="layout:main:resize",
- navigate-highlights="true"
- )
-
- .diff.diff-binary(ng-show="history.diff.binary")
- .toolbar.toolbar-alt
- span.name
- strong {{history.diff.pathname}}
- .alert.alert-info We're still working on showing image and binary changes, sorry. Stay tuned!
-
- .diff-deleted.text-centered(
- ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
- )
- p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
- p
- a.btn.btn-primary.btn-lg(
- href,
- ng-click="restoreDeletedDoc()",
- ng-disabled="history.diff.restoreInProgress"
- ) #{translate("restore")}
-
- .diff-deleted.text-centered(
- ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
- )
- p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
- p.text-serif #{translate("file_restored_back_to_editor")}
- p
- a.btn.btn-default(
- href,
- ng-click="backToEditorAfterRestore()",
- ) #{translate("file_restored_back_to_editor_btn")}
-
- .loading-panel(ng-show="history.diff.loading")
- i.fa.fa-spin.fa-refresh
- | #{translate("loading")}...
- .error-panel(ng-show="history.diff.error")
- .alert.alert-danger #{translate("generic_something_went_wrong")}
+ include ./history/diffPanelV1
+ include ./history/diffPanelV2
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
.modal-header
diff --git a/services/web/app/views/project/editor/history/diffPanelV1.pug b/services/web/app/views/project/editor/history/diffPanelV1.pug
new file mode 100644
index 0000000000..1720f48b59
--- /dev/null
+++ b/services/web/app/views/project/editor/history/diffPanelV1.pug
@@ -0,0 +1,58 @@
+.diff-panel.full-size(ng-if="!history.isV2", ng-controller="HistoryDiffController")
+ .diff(
+ ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary"
+ )
+ .toolbar.toolbar-alt
+ span.name
+ | {{history.diff.highlights.length}}
+ ng-pluralize(
+ count="history.diff.highlights.length",
+ when="{\
+ 'one': 'change',\
+ 'other': 'changes'\
+ }"
+ )
+ | in {{history.diff.pathname}}
+ .toolbar-right
+ a.btn.btn-danger.btn-sm(
+ href,
+ ng-click="openRestoreDiffModal()"
+ ) #{translate("restore_to_before_these_changes")}
+ .diff-editor.hide-ace-cursor(
+ ace-editor="history",
+ theme="settings.theme",
+ font-size="settings.fontSize",
+ text="history.diff.text",
+ highlights="history.diff.highlights",
+ read-only="true",
+ resize-on="layout:main:resize",
+ navigate-highlights="true"
+ )
+
+ .diff-deleted.text-centered(
+ ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
+ )
+ p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
+ p
+ a.btn.btn-primary.btn-lg(
+ href,
+ ng-click="restoreDeletedDoc()",
+ ng-disabled="history.diff.restoreInProgress"
+ ) #{translate("restore")}
+
+ .diff-deleted.text-centered(
+ ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
+ )
+ p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
+ p.text-serif #{translate("file_restored_back_to_editor")}
+ p
+ a.btn.btn-default(
+ href,
+ ng-click="backToEditorAfterRestore()",
+ ) #{translate("file_restored_back_to_editor_btn")}
+
+ .loading-panel(ng-show="history.diff.loading")
+ i.fa.fa-spin.fa-refresh
+ | #{translate("loading")}...
+ .error-panel(ng-show="history.diff.error")
+ .alert.alert-danger #{translate("generic_something_went_wrong")}
\ No newline at end of file
diff --git a/services/web/app/views/project/editor/history/diffPanelV2.pug b/services/web/app/views/project/editor/history/diffPanelV2.pug
new file mode 100644
index 0000000000..415d587155
--- /dev/null
+++ b/services/web/app/views/project/editor/history/diffPanelV2.pug
@@ -0,0 +1,50 @@
+.diff-panel.full-size(ng-if="history.isV2", ng-controller="HistoryV2DiffController")
+ .diff(
+ ng-if="!!history.diff && !history.diff.loading && !history.diff.error",
+ ng-class="{ 'diff-binary': history.diff.binary }"
+ )
+ .toolbar.toolbar-alt
+ span.name(ng-if="history.diff.binary")
+ strong {{history.diff.pathname}}
+ span.name(ng-if="!history.diff.binary")
+ | {{history.diff.highlights.length}}
+ ng-pluralize(
+ count="history.diff.highlights.length",
+ when="{\
+ 'one': 'change',\
+ 'other': 'changes'\
+ }"
+ )
+ | in {{history.diff.pathname}}
+ .toolbar-right(ng-if="history.selection.docs[history.selection.pathname].deletedAtV")
+ button.btn.btn-danger.btn-sm(
+ ng-click="restoreDeletedFile()"
+ ng-show="!restoreState.error"
+ ng-disabled="restoreState.inflight"
+ )
+ i.fa.fa-fw.fa-step-backward
+ span(ng-show="!restoreState.inflight")
+ | Restore this deleted file
+ span(ng-show="restoreState.inflight")
+ | Restoring...
+ span.text-danger(ng-show="restoreState.error")
+ | Error restoring, sorry
+ .diff-editor.hide-ace-cursor(
+ ng-if="!history.diff.binary"
+ ace-editor="history",
+ theme="settings.theme",
+ font-size="settings.fontSize",
+ text="history.diff.text",
+ highlights="history.diff.highlights",
+ read-only="true",
+ resize-on="layout:main:resize",
+ navigate-highlights="true"
+ )
+ .alert.alert-info(ng-if="history.diff.binary")
+ | We're still working on showing image and binary changes, sorry. Stay tuned!
+
+ .loading-panel(ng-show="history.diff.loading")
+ i.fa.fa-spin.fa-refresh
+ | #{translate("loading")}...
+ .error-panel(ng-show="history.diff.error")
+ .alert.alert-danger #{translate("generic_something_went_wrong")}
\ No newline at end of file
diff --git a/services/web/nodemon.frontend.json b/services/web/nodemon.frontend.json
index a5897558c0..d0f321fa14 100644
--- a/services/web/nodemon.frontend.json
+++ b/services/web/nodemon.frontend.json
@@ -5,7 +5,7 @@
],
"verbose": true,
"legacyWatch": true,
- "exec": "make compile",
+ "exec": "make compile || exit 1",
"watch": [
"public/coffee/",
"public/stylesheets/"
diff --git a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee
index 02283bc49e..ebecf1132e 100644
--- a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee
+++ b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee
@@ -8,6 +8,7 @@ define [
@openFile(entity)
openFile: (file) ->
+ @ide.fileTreeManager.selectEntity(file)
@$scope.ui.view = "file"
@$scope.openFile = null
@$scope.$apply()
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index 8c77d97965..cdc39ffd05 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -4,6 +4,7 @@ define [
"ide/history/util/displayNameForUser"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
+ "ide/history/controllers/HistoryV2DiffController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager, displayNameForUser) ->
class HistoryManager
diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
index 574190c870..72f4c79bdd 100644
--- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
@@ -49,6 +49,13 @@ define [
diff: null
}
+ restoreFile: (version, pathname) ->
+ url = "/project/#{@$scope.project_id}/restore_file"
+ @ide.$http.post(url, {
+ version, pathname,
+ _csrf: window.csrfToken
+ })
+
MAX_RECENT_UPDATES_TO_SELECT: 5
autoSelectRecentUpdates: () ->
return if @$scope.history.updates.length == 0
@@ -204,7 +211,7 @@ define [
# Map of original pathname -> doc summary
docs_summary = Object.create(null)
- updatePathnameWithUpdateVersions = (pathname, update, deleted) ->
+ updatePathnameWithUpdateVersions = (pathname, update, deletedAtV) ->
# docs_summary is indexed by the original pathname the doc
# had at the start, so we have to look this up from the current
# pathname via original_pathname first
@@ -222,8 +229,8 @@ define [
doc_summary.toV,
update.toV
)
- if deleted?
- doc_summary.deleted = true
+ if deletedAtV?
+ doc_summary.deletedAtV = deletedAtV
# Put updates in ascending chronological order
updates = updates.slice().reverse()
@@ -241,7 +248,7 @@ define [
updatePathnameWithUpdateVersions(add.pathname, update)
if project_op.remove?
remove = project_op.remove
- updatePathnameWithUpdateVersions(remove.pathname, update, true)
+ updatePathnameWithUpdateVersions(remove.pathname, update, project_op.atV)
return docs_summary
diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee
new file mode 100644
index 0000000000..89cf3975ec
--- /dev/null
+++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee
@@ -0,0 +1,39 @@
+define [
+ "base"
+], (App) ->
+ App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
+ $scope.restoreState =
+ inflight: false
+ error: false
+
+ $scope.restoreDeletedFile = () ->
+ pathname = $scope.history.selection.pathname
+ return if !pathname?
+ version = $scope.history.selection.docs[pathname]?.deletedAtV
+ return if !version?
+ event_tracking.sendMB "history-v2-restore-deleted"
+ $scope.restoreState.inflight = true
+ ide.historyManager
+ .restoreFile(version, pathname)
+ .then (response) ->
+ { data } = response
+ openEntity(data)
+ .catch () ->
+ ide.showGenericMessageModal('Sorry, something went wrong with the restore')
+ .finally () ->
+ $scope.restoreState.inflight = false
+
+ openEntity = (data) ->
+ iterations = 0
+ {id, type} = data
+ do tryOpen = () ->
+ if iterations > 5
+ return
+ entity = ide.fileTreeManager.findEntityById(id)
+ if entity? and type == 'doc'
+ ide.editorManager.openDoc(entity)
+ if entity? and type == 'file'
+ ide.binaryFilesManager.openFile(entity)
+ else
+ setTimeout(tryOpen, 500)
+
\ No newline at end of file
diff --git a/services/web/test/acceptance/coffee/RestoringFilesTest.coffee b/services/web/test/acceptance/coffee/RestoringFilesTest.coffee
new file mode 100644
index 0000000000..5abdd3e11f
--- /dev/null
+++ b/services/web/test/acceptance/coffee/RestoringFilesTest.coffee
@@ -0,0 +1,195 @@
+async = require "async"
+expect = require("chai").expect
+_ = require 'underscore'
+
+ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js"
+
+User = require "./helpers/User"
+MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
+MockDocstoreApi = require "./helpers/MockDocstoreApi"
+MockFileStoreApi = require "./helpers/MockFileStoreApi"
+
+describe "RestoringFiles", ->
+ before (done) ->
+ @owner = new User()
+ @owner.login (error) =>
+ throw error if error?
+ @owner.createProject "example-project", {template: "example"}, (error, @project_id) =>
+ throw error if error?
+ done()
+
+ describe "restoring a deleted doc", ->
+ beforeEach (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ @doc = _.find project.rootFolder[0].docs, (doc) ->
+ doc.name == 'main.tex'
+ @owner.request {
+ method: "DELETE",
+ url: "/project/#{@project_id}/doc/#{@doc._id}",
+ }, (error, response, body) =>
+ throw error if error?
+ expect(response.statusCode).to.equal 204
+ @owner.request {
+ method: "POST",
+ url: "/project/#{@project_id}/doc/#{@doc._id}/restore"
+ json:
+ name: "main.tex"
+ }, (error, response, body) =>
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ expect(body.doc_id).to.exist
+ @restored_doc_id = body.doc_id
+ done()
+
+ it 'should have restored the doc', (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ restored_doc = _.find project.rootFolder[0].docs, (doc) ->
+ doc.name == 'main.tex'
+ expect(restored_doc._id.toString()).to.equal @restored_doc_id
+ expect(@doc._id).to.not.equal @restored_doc_id
+ # console.log @doc_id, @restored_doc_id, MockDocstoreApi.docs[@project_id]
+ expect(MockDocstoreApi.docs[@project_id][@restored_doc_id].lines).to.deep.equal(
+ MockDocstoreApi.docs[@project_id][@doc._id].lines
+ )
+ done()
+
+ describe "restoring from v2 history", ->
+ describe "restoring a text file", ->
+ beforeEach (done) ->
+ MockProjectHistoryApi.addOldFile(@project_id, 42, "foo.tex", "hello world, this is foo.tex!")
+ @owner.request {
+ method: "POST",
+ url: "/project/#{@project_id}/restore_file",
+ json:
+ pathname: "foo.tex"
+ version: 42
+ }, (error, response, body) ->
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ done()
+
+ it "should have created a doc", (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ doc = _.find project.rootFolder[0].docs, (doc) ->
+ doc.name == 'foo.tex'
+ doc = MockDocstoreApi.docs[@project_id][doc._id]
+ expect(doc.lines).to.deep.equal [
+ "hello world, this is foo.tex!"
+ ]
+ done()
+
+ describe "restoring a binary file", ->
+ beforeEach (done) ->
+ MockProjectHistoryApi.addOldFile(@project_id, 42, "image.png", "Mock image.png content")
+ @owner.request {
+ method: "POST",
+ url: "/project/#{@project_id}/restore_file",
+ json:
+ pathname: "image.png"
+ version: 42
+ }, (error, response, body) ->
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ done()
+
+ it "should have created a file", (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ file = _.find project.rootFolder[0].fileRefs, (file) ->
+ file.name == 'image.png'
+ file = MockFileStoreApi.files[@project_id][file._id]
+ expect(file.content).to.equal "Mock image.png content"
+ done()
+
+ describe "restoring to a directory that exists", ->
+ beforeEach (done) ->
+ MockProjectHistoryApi.addOldFile(@project_id, 42, "foldername/foo2.tex", "hello world, this is foo-2.tex!")
+ @owner.request.post {
+ uri: "project/#{@project_id}/folder",
+ json:
+ name: 'foldername'
+ }, (error, response, body) =>
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ @owner.request {
+ method: "POST",
+ url: "/project/#{@project_id}/restore_file",
+ json:
+ pathname: "foldername/foo2.tex"
+ version: 42
+ }, (error, response, body) ->
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ done()
+
+ it "should have created the doc in the named folder", (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ folder = _.find project.rootFolder[0].folders, (folder) ->
+ folder.name == 'foldername'
+ doc = _.find folder.docs, (doc) ->
+ doc.name == 'foo2.tex'
+ doc = MockDocstoreApi.docs[@project_id][doc._id]
+ expect(doc.lines).to.deep.equal [
+ "hello world, this is foo-2.tex!"
+ ]
+ done()
+
+ describe "restoring to a directory that no longer exists", ->
+ beforeEach (done) ->
+ MockProjectHistoryApi.addOldFile(@project_id, 42, "nothere/foo3.tex", "hello world, this is foo-3.tex!")
+ @owner.request {
+ method: "POST",
+ url: "/project/#{@project_id}/restore_file",
+ json:
+ pathname: "nothere/foo3.tex"
+ version: 42
+ }, (error, response, body) ->
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ done()
+
+ it "should have created the folder and restored the doc to it", (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ folder = _.find project.rootFolder[0].folders, (folder) ->
+ folder.name == 'nothere'
+ expect(folder).to.exist
+ doc = _.find folder.docs, (doc) ->
+ doc.name == 'foo3.tex'
+ doc = MockDocstoreApi.docs[@project_id][doc._id]
+ expect(doc.lines).to.deep.equal [
+ "hello world, this is foo-3.tex!"
+ ]
+ done()
+
+ describe "restoring to a filename that already exists", ->
+ it "should have created the file with a timestamp appended", ->
+ beforeEach (done) ->
+ MockProjectHistoryApi.addOldFile(@project_id, 42, "main.tex", "hello world, this is main.tex!")
+ @owner.request {
+ method: "POST",
+ url: "/project/#{@project_id}/restore_file",
+ json:
+ pathname: "main.tex"
+ version: 42
+ }, (error, response, body) ->
+ throw error if error?
+ expect(response.statusCode).to.equal 200
+ done()
+
+ it "should have created the doc in the root folder", (done) ->
+ @owner.getProject @project_id, (error, project) =>
+ throw error if error?
+ doc = _.find project.rootFolder[0].docs, (doc) ->
+ doc.name.match(/main \(Restored on/)
+ expect(doc).to.exist
+ doc = MockDocstoreApi.docs[@project_id][doc._id]
+ expect(doc.lines).to.deep.equal [
+ "hello world, this is main.tex!"
+ ]
+ done()
+
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
index a9a2c49875..395fe88a40 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
@@ -36,7 +36,7 @@ module.exports = MockDocUpdaterApi =
res.sendStatus 200
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
- res.send 204
+ res.sendStatus 204
app.listen 3003, (error) ->
throw error if error?
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
index 631538fd89..96388f559b 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
@@ -23,15 +23,23 @@ module.exports = MockDocStoreApi =
docs = (doc for doc_id, doc of @docs[req.params.project_id])
res.send JSON.stringify docs
+ app.get "/project/:project_id/doc/:doc_id", (req, res, next) =>
+ {project_id, doc_id} = req.params
+ doc = @docs[project_id][doc_id]
+ if doc.deleted and !req.query.include_deleted
+ res.sendStatus 404
+ else
+ res.send JSON.stringify doc
+
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
{project_id, doc_id} = req.params
if !@docs[project_id]?
- res.send 404
+ res.sendStatus 404
else if !@docs[project_id][doc_id]?
- res.send 404
+ res.sendStatus 404
else
- @docs[project_id][doc_id] = undefined
- res.send 204
+ @docs[project_id][doc_id].deleted = true
+ res.sendStatus 204
app.listen 3016, (error) ->
throw error if error?
diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee
index 9027e22468..381d7ab272 100644
--- a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee
@@ -4,10 +4,23 @@ app = express()
module.exports = MockProjectHistoryApi =
docs: {}
+ oldFiles: {}
+
+ addOldFile: (project_id, version, pathname, content) ->
+ @oldFiles["#{project_id}:#{version}:#{pathname}"] = content
+
run: () ->
app.post "/project", (req, res, next) =>
res.json project: id: 1
+ app.get "/project/:project_id/version/:version/:pathname", (req, res, next) =>
+ {project_id, version, pathname} = req.params
+ key = "#{project_id}:#{version}:#{pathname}"
+ if @oldFiles[key]?
+ res.send @oldFiles[key]
+ else
+ res.send 404
+
app.listen 3054, (error) ->
throw error if error?
.on "error", (error) ->
diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
index 5846d6284a..08e9482778 100644
--- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
@@ -164,35 +164,6 @@ describe "EditorHttpController", ->
it "should return false in the callback", ->
@callback.calledWith(null, null, false).should.equal true
- describe "restoreDoc", ->
- beforeEach ->
- @req.params =
- Project_id: @project_id
- doc_id: @doc_id
- @req.body =
- name: @name = "doc-name"
- @ProjectEntityUpdateHandler.restoreDoc = sinon.stub().callsArgWith(3, null,
- @doc = { "mock": "doc", _id: @new_doc_id = "new-doc-id" }
- @folder_id = "mock-folder-id"
- )
- @EditorRealTimeController.emitToRoom = sinon.stub()
- @EditorHttpController.restoreDoc @req, @res
-
- it "should restore the doc", ->
- @ProjectEntityUpdateHandler.restoreDoc
- .calledWith(@project_id, @doc_id, @name)
- .should.equal true
-
- it "should the real-time clients about the new doc", ->
- @EditorRealTimeController.emitToRoom
- .calledWith(@project_id, 'reciveNewDoc', @folder_id, @doc)
- .should.equal true
-
- it "should return the new doc id", ->
- @res.json
- .calledWith(doc_id: @new_doc_id)
- .should.equal true
-
describe "addDoc", ->
beforeEach ->
@doc = { "mock": "doc" }
diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
index 34039ee0c4..d5777f5490 100644
--- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
+++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
@@ -22,6 +22,7 @@ describe "HistoryController", ->
"./HistoryManager": @HistoryManager = {}
"../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {}
"../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {}
+ "./RestoreManager": @RestoreManager = {}
@settings.apis =
trackchanges:
enabled: false
diff --git a/services/web/test/unit/coffee/History/RestoreManagerTests.coffee b/services/web/test/unit/coffee/History/RestoreManagerTests.coffee
new file mode 100644
index 0000000000..b15cdab946
--- /dev/null
+++ b/services/web/test/unit/coffee/History/RestoreManagerTests.coffee
@@ -0,0 +1,126 @@
+SandboxedModule = require('sandboxed-module')
+assert = require('assert')
+require('chai').should()
+expect = require('chai').expect
+sinon = require('sinon')
+modulePath = require('path').join __dirname, '../../../../app/js/Features/History/RestoreManager'
+Errors = require '../../../../app/js/Features/Errors/Errors'
+tk = require("timekeeper")
+moment = require('moment')
+
+describe 'RestoreManager', ->
+ beforeEach ->
+ @RestoreManager = SandboxedModule.require modulePath, requires:
+ '../../infrastructure/FileWriter': @FileWriter = {}
+ '../Uploads/FileSystemImportManager': @FileSystemImportManager = {}
+ '../Project/ProjectLocator': @ProjectLocator = {}
+ '../Errors/Errors': Errors
+ '../Project/ProjectEntityHandler': @ProjectEntityHandler = {}
+ '../Editor/EditorController': @EditorController = {}
+ 'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()}
+ @user_id = 'mock-user-id'
+ @project_id = 'mock-project-id'
+ @version = 42
+ @callback = sinon.stub()
+ tk.freeze Date.now() # freeze the time for these tests
+
+ afterEach ->
+ tk.reset()
+
+ describe 'restoreFileFromV2', ->
+ beforeEach ->
+ @RestoreManager._writeFileVersionToDisk = sinon.stub().yields(null, @fsPath = "/tmp/path/on/disk")
+ @RestoreManager._findFolderOrRootFolderId = sinon.stub().yields(null, @folder_id = 'mock-folder-id')
+ @FileSystemImportManager.addEntity = sinon.stub().yields(null, @entity = 'mock-entity')
+
+ describe "with a file not in a folder", ->
+ beforeEach ->
+ @pathname = 'foo.tex'
+ @RestoreManager.restoreFileFromV2 @user_id, @project_id, @version, @pathname, @callback
+
+ it 'should write the file version to disk', ->
+ @RestoreManager._writeFileVersionToDisk
+ .calledWith(@project_id, @version, @pathname)
+ .should.equal true
+
+ it 'should find the root folder', ->
+ @RestoreManager._findFolderOrRootFolderId
+ .calledWith(@project_id, "")
+ .should.equal true
+
+ it 'should add the entity', ->
+ @FileSystemImportManager.addEntity
+ .calledWith(@user_id, @project_id, @folder_id, 'foo.tex', @fsPath, false)
+ .should.equal true
+
+ it 'should call the callback with the entity', ->
+ @callback.calledWith(null, @entity).should.equal true
+
+ describe "with a file in a folder", ->
+ beforeEach ->
+ @pathname = 'foo/bar.tex'
+ @RestoreManager.restoreFileFromV2 @user_id, @project_id, @version, @pathname, @callback
+
+ it 'should find the folder', ->
+ @RestoreManager._findFolderOrRootFolderId
+ .calledWith(@project_id, "foo")
+ .should.equal true
+
+ it 'should add the entity by its basename', ->
+ @FileSystemImportManager.addEntity
+ .calledWith(@user_id, @project_id, @folder_id, 'bar.tex', @fsPath, false)
+ .should.equal true
+
+ describe '_findFolderOrRootFolderId', ->
+ describe 'with a folder that exists', ->
+ beforeEach ->
+ @ProjectLocator.findElementByPath = sinon.stub().yields(null, {_id: @folder_id = 'mock-folder-id'}, 'folder')
+ @RestoreManager._findFolderOrRootFolderId @project_id, 'folder_name', @callback
+
+ it 'should look up the folder', ->
+ @ProjectLocator.findElementByPath
+ .calledWith({project_id: @project_id, path: 'folder_name'})
+ .should.equal true
+
+ it 'should return the folder_id', ->
+ @callback.calledWith(null, @folder_id).should.equal true
+
+ describe "with a folder that doesn't exist", ->
+ beforeEach ->
+ @ProjectLocator.findElementByPath = sinon.stub().yields(new Errors.NotFoundError())
+ @RestoreManager._findFolderOrRootFolderId @project_id, 'folder_name', @callback
+
+ it 'should return null', ->
+ @callback.calledWith(null, null).should.equal true
+
+ describe '_addEntityWithUniqueName', ->
+ beforeEach ->
+ @addEntityWithName = sinon.stub()
+ @name = 'foo.tex'
+
+ describe 'with a valid name', ->
+ beforeEach ->
+ @addEntityWithName.yields(null, @entity = 'mock-entity')
+ @RestoreManager._addEntityWithUniqueName @addEntityWithName, @name, @callback
+
+ it 'should add the entity', ->
+ @addEntityWithName.calledWith(@name).should.equal true
+
+ it 'should return the entity', ->
+ @callback.calledWith(null, @entity).should.equal true
+
+ describe "with an invalid name", ->
+ beforeEach ->
+ @addEntityWithName.onFirstCall().yields(new Errors.InvalidNameError())
+ @addEntityWithName.onSecondCall().yields(null, @entity = 'mock-entity')
+ @RestoreManager._addEntityWithUniqueName @addEntityWithName, @name, @callback
+
+ it 'should try to add the entity with its original name', ->
+ @addEntityWithName.calledWith('foo.tex').should.equal true
+
+ it 'should try to add the entity with a unique name', ->
+ date = moment(new Date()).format('Do MMM YY H:mm:ss')
+ @addEntityWithName.calledWith("foo (Restored on #{date}).tex").should.equal true
+
+ it 'should return the entity', ->
+ @callback.calledWith(null, @entity).should.equal true
diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee
index 09d0d274fb..788a32cd3b 100644
--- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee
@@ -251,27 +251,6 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith({_id : project_id}, {$unset : {rootDoc_id: true}})
.should.equal true
- describe "restoreDoc", ->
- beforeEach ->
- @doc = { "mock": "doc" }
- @ProjectEntityHandler.getDoc = sinon.stub().yields(null, @docLines)
- @ProjectEntityUpdateHandler.addDoc = sinon.stub().yields(null, @doc, folder_id)
-
- @ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, @docName, @callback
-
- it 'should get the doc lines', ->
- @ProjectEntityHandler.getDoc
- .calledWith(project_id, doc_id, include_deleted: true)
- .should.equal true
-
- it "should add a new doc with these doc lines", ->
- @ProjectEntityUpdateHandler.addDoc
- .calledWith(project_id, null, @docName, @docLines)
- .should.equal true
-
- it "should call the callback with the new folder and doc", ->
- @callback.calledWith(null, @doc, folder_id).should.equal true
-
describe 'addDoc', ->
beforeEach ->
@path = "/path/to/doc"
diff --git a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee
index 358268310e..9d0e103e0c 100644
--- a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee
+++ b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee
@@ -130,12 +130,13 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
project_ops: [{
remove:
pathname: "main.tex"
+ atV: 2
}]
fromV: 1, toV: 2
}])
expect(result).to.deep.equal({
- "main.tex": { fromV: 0, toV: 2, deleted: true }
+ "main.tex": { fromV: 0, toV: 2, deletedAtV: 2 }
})
it "should track single deletions", ->
@@ -143,10 +144,11 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
project_ops: [{
remove:
pathname: "main.tex"
+ atV: 1
}]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
- "main.tex": { fromV: 0, toV: 1, deleted: true }
+ "main.tex": { fromV: 0, toV: 1, deletedAtV: 1 }
})