Files
overleaf-cep/services/project-history/app/js/FileTreeDiffGenerator.js
T
Jakob AckermannandClaude Opus 5 7ae8db6b12 [editor-core] extract shared pieces for history-ot in editor (#36828)
* [editor-core] share the pieces the editor, real-time and history-v1 all need

Project-level history-OT has the browser, real-time and history-v1 all reason
about the same changes, so the rules they reason with move into the shared
library rather than being reimplemented on each side.

- `origin/editor_origin.js` plus `EDITOR_ORIGIN_KIND` and its schema entry: the
  origin an editor client's change carries, holding the editorId that scopes a
  change's timestamp.
- `change_identity.js`: how a submission is recognised when it comes back, so a
  resend of a change that already landed is deduplicated rather than applied
  twice.
- `rebase.js`: transforming a change onto concurrent ones, in the same order on
  both sides, which is what lets the server acknowledge a change with only the
  version it landed at.
- `text_file_defaults.js`: the doc-vs-file extension lists, now read from here
  by web's settings instead of being spelled out again.
- `blob_string_length.js`: the string length of a blob, used by history-v1's
  blob store and by web when it writes an uploaded file into history.
- `file_tree_diff.js`: file-tree operations to a diff, used by
  project-history's FileTreeDiffGenerator.
- `Blob.EMPTY_HASH`, so callers can answer for the empty blob without storing
  it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [project-history] build the file-tree diff with the shared helper

FileTreeDiffGenerator's hand-rolled walk over add/edit/move/delete operations
becomes a call to `buildFileTreeDiff` from overleaf-editor-core, so the editor
and project-history read a file-tree change the same way. Adds the unit tests
the decaffeinated original never had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [web] read the editable-file lists from overleaf-editor-core

web's settings spelled out the text extensions and editable filenames that
decide whether a file becomes a doc, and every other service that has to make
that call carried its own copy. The lists now live in overleaf-editor-core and
web builds settings.textExtensions and settings.editableFilenames from them,
keeping the ADDITIONAL_TEXT_EXTENSIONS override for Server CE/Pro on top.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [history-v1] read a blob's string length from overleaf-editor-core

Two copies of getStringLengthOfFile lived here, the one in recover_zip.js
carrying a TODO asking for exactly this. Both now import the shared
implementation, which the blob store keeps re-exporting for backupArchiver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GitOrigin-RevId: 31948332f416f56848e3b0dfb5142a4c9f93f508
2026-09-17 12:06:17 +00:00

103 lines
3.5 KiB
JavaScript

import Core from 'overleaf-editor-core'
import { buildFileTreeDiff } from 'overleaf-editor-core/lib/file_tree_diff.js'
import * as Errors from './Errors.js'
/**
* @import { FileTreeDiffEntry } from 'overleaf-editor-core/lib/file_tree_diff'
* @import { Snapshot } from 'overleaf-editor-core'
*/
/**
* One file of the diff. Files without an operation are unchanged.
*
* @typedef {object} FileTreeDiffFile
* @property {string} pathname the pathname of the file at fromVersion, or the
* pathname it was added at
* @property {string} [newPathname] the pathname of a renamed file at toVersion
* @property {'added' | 'edited' | 'renamed' | 'removed'} [operation]
* @property {number} [deletedAtV] version a removed file was removed at
* @property {boolean | null} [editable] null when it is not known
*/
/**
* Describe how the file tree changed between two versions of a chunk.
*
* @param {object} rawChunk chunk covering fromVersion to toVersion
* @param {number} fromVersion
* @param {number} toVersion
* @return {FileTreeDiffFile[]} one entry per file
*/
export function buildDiff(rawChunk, fromVersion, toVersion) {
const chunk = Core.Chunk.fromRaw(rawChunk.chunk)
const chunkStartVersion = chunk.getStartVersion()
const changes = chunk.getChanges()
// The chunk starts at chunkStartVersion, so bring its snapshot forward to the
// file tree at fromVersion, which the diff is reported against.
const initialSnapshot = chunk.getSnapshot()
initialSnapshot.applyAll(changes.slice(0, fromVersion - chunkStartVersion))
const { entries } = buildFileTreeDiff(
changes.slice(
fromVersion - chunkStartVersion,
toVersion - chunkStartVersion
),
{
initialPathnames: initialSnapshot.getFilePathnames(),
onMoveCollision: (entry, operation) => {
throw new Errors.InconsistentChunkError(
'trying to move to file that already exists',
{
pathname: operation.getPathname(),
newPathname: operation.getNewPathname(),
}
)
},
}
)
return Array.from(entries.values(), entry =>
_buildDiffEntry(entry, initialSnapshot, fromVersion)
)
}
/**
* @param {FileTreeDiffEntry} entry
* @param {Snapshot} initialSnapshot file tree at fromVersion
* @param {number} fromVersion
* @return {FileTreeDiffFile}
*/
function _buildDiffEntry(entry, initialSnapshot, fromVersion) {
const { origin } = entry
const pathname = entry.chain[entry.chain.length - 1]
const renamed = origin != null && pathname !== origin
/** @type {FileTreeDiffFile} */
const diffEntry = { pathname: origin ?? pathname }
if (renamed) {
diffEntry.newPathname = pathname
}
if (entry.deletedAtChangeIndex != null) {
diffEntry.operation = 'removed'
diffEntry.deletedAtV = fromVersion + entry.deletedAtChangeIndex
} else if (origin == null) {
diffEntry.operation = 'added'
} else if (renamed) {
diffEntry.operation = 'renamed'
} else if (entry.edited) {
diffEntry.operation = 'edited'
}
// Report the editability of the file, so that the frontend knows whether it
// can display a diff for it. It is left out for files that were edited before
// anything else happened to them: an edit implies that the file is editable.
const editedFirst = entry.file == null && entry.firstEditedAtChainIndex === 0
const file =
entry.file ?? (origin != null ? initialSnapshot.getFile(origin) : null)
if (file != null && !editedFirst) {
diffEntry.editable = file.isEditable()
}
return diffEntry
}