Merge pull request #13039 from overleaf/td-history-auto-select-file-ignore-binary

History migration: Ignore binary files when auto-selecting file to display in history view

GitOrigin-RevId: 7d2a314cdb3d54e4e2292a95e7633e3829ea931f
This commit is contained in:
Tim Down 2023-05-25 11:20:57 +01:00 committed by Copybot
parent 9a273949fd
commit 66dc566752
8 changed files with 211 additions and 77 deletions

View file

@ -54,21 +54,20 @@ function _getInitialDiffSnapshot(chunk, fromVersion) {
// with nothing in the diff marked as changed. // with nothing in the diff marked as changed.
// Use a bare object to protect against reserved names. // Use a bare object to protect against reserved names.
const diff = Object.create(null) const diff = Object.create(null)
const pathnames = _getInitialPathnames(chunk, fromVersion) const files = _getInitialFiles(chunk, fromVersion)
for (const pathname of Array.from(pathnames)) { for (const [pathname, file] of Object.entries(files)) {
diff[pathname] = { pathname } diff[pathname] = { pathname, editable: file.isEditable() }
} }
return diff return diff
} }
function _getInitialPathnames(chunk, fromVersion) { function _getInitialFiles(chunk, fromVersion) {
const snapshot = chunk.getSnapshot() const snapshot = chunk.getSnapshot()
const changes = chunk const changes = chunk
.getChanges() .getChanges()
.slice(0, fromVersion - chunk.getStartVersion()) .slice(0, fromVersion - chunk.getStartVersion())
snapshot.applyAll(changes) snapshot.applyAll(changes)
const pathnames = snapshot.getFilePathnames() return snapshot.fileMap.files
return pathnames
} }
function _applyAddFileToDiff(diff, operation) { function _applyAddFileToDiff(diff, operation) {
@ -88,6 +87,7 @@ function _applyAddFileToDiff(diff, operation) {
return (diff[operation.pathname] = { return (diff[operation.pathname] = {
pathname: operation.pathname, pathname: operation.pathname,
operation: 'added', operation: 'added',
editable: operation.file.isEditable(),
}) })
} }
} }

View file

@ -123,6 +123,7 @@ describe('FileTree Diffs', function () {
{ {
file: { file: {
hash: sha('new-sha'), hash: sha('new-sha'),
stringLength: 42,
}, },
pathname: 'added.tex', pathname: 'added.tex',
}, },
@ -155,15 +156,18 @@ describe('FileTree Diffs', function () {
pathname: 'deleted.tex', pathname: 'deleted.tex',
operation: 'removed', operation: 'removed',
deletedAtV: 5, deletedAtV: 5,
editable: true,
}, },
{ {
newPathname: 'newName.tex', newPathname: 'newName.tex',
pathname: 'renamed.tex', pathname: 'renamed.tex',
operation: 'renamed', operation: 'renamed',
editable: true,
}, },
{ {
pathname: 'added.tex', pathname: 'added.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
], ],
}) })
@ -269,6 +273,7 @@ describe('FileTree Diffs', function () {
{ {
file: { file: {
hash: sha('new-sha'), hash: sha('new-sha'),
stringLength: 42,
}, },
pathname: 'added.tex', pathname: 'added.tex',
}, },
@ -313,20 +318,24 @@ describe('FileTree Diffs', function () {
}, },
{ {
pathname: 'baz.tex', pathname: 'baz.tex',
editable: true,
}, },
{ {
pathname: 'deleted.tex', pathname: 'deleted.tex',
operation: 'removed', operation: 'removed',
deletedAtV: 4, deletedAtV: 4,
editable: true,
}, },
{ {
newPathname: 'newName.tex', newPathname: 'newName.tex',
pathname: 'renamed.tex', pathname: 'renamed.tex',
operation: 'renamed', operation: 'renamed',
editable: true,
}, },
{ {
pathname: 'added.tex', pathname: 'added.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
], ],
}) })
@ -391,6 +400,7 @@ describe('FileTree Diffs', function () {
newPathname: 'three.tex', newPathname: 'three.tex',
pathname: 'one.tex', pathname: 'one.tex',
operation: 'renamed', operation: 'renamed',
editable: true,
}, },
], ],
}) })
@ -455,6 +465,7 @@ describe('FileTree Diffs', function () {
diff: [ diff: [
{ {
pathname: 'one.tex', pathname: 'one.tex',
editable: true,
}, },
], ],
}) })
@ -523,6 +534,7 @@ describe('FileTree Diffs', function () {
pathname: 'two.tex', pathname: 'two.tex',
newPathname: 'one.tex', newPathname: 'one.tex',
operation: 'renamed', operation: 'renamed',
editable: true,
}, },
], ],
}) })
@ -547,6 +559,7 @@ describe('FileTree Diffs', function () {
pathname: 'one.tex', pathname: 'one.tex',
file: { file: {
hash: sha('mock-sha'), hash: sha('mock-sha'),
stringLength: 42,
}, },
}, },
], ],
@ -583,6 +596,7 @@ describe('FileTree Diffs', function () {
{ {
pathname: 'two.tex', pathname: 'two.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
], ],
}) })

View file

@ -1,24 +1,30 @@
import { FileOperation } from './file-operation' import { FileOperation } from './file-operation'
export interface FileUnchanged { interface File {
pathname: string pathname: string
} }
export interface FileAdded extends FileUnchanged { export interface FileWithEditable extends File {
editable: boolean
}
export type FileUnchanged = FileWithEditable
export interface FileAdded extends FileWithEditable {
operation: Extract<FileOperation, 'added'> operation: Extract<FileOperation, 'added'>
} }
export interface FileRemoved extends FileUnchanged { export interface FileRemoved extends FileWithEditable {
operation: Extract<FileOperation, 'removed'> operation: Extract<FileOperation, 'removed'>
newPathname?: string newPathname?: string
deletedAtV: number deletedAtV: number
} }
export interface FileEdited extends FileUnchanged { export interface FileEdited extends File {
operation: Extract<FileOperation, 'edited'> operation: Extract<FileOperation, 'edited'>
} }
export interface FileRenamed extends FileUnchanged { export interface FileRenamed extends FileWithEditable {
newPathname?: string newPathname?: string
oldPathname?: string oldPathname?: string
operation: Extract<FileOperation, 'renamed'> operation: Extract<FileOperation, 'renamed'>

View file

@ -1,12 +1,12 @@
import _ from 'lodash'
import type { Nullable } from '../../../../../types/utils' import type { Nullable } from '../../../../../types/utils'
import type { FileDiff } from '../services/types/file' import type { FileDiff } from '../services/types/file'
import type { FileOperation } from '../services/types/file-operation' import type { FileOperation } from '../services/types/file-operation'
import type { LoadedUpdate, Version } from '../services/types/update' import type { LoadedUpdate, Version } from '../services/types/update'
import { fileFinalPathname } from './file-diff' import { fileFinalPathname, isFileEditable } from './file-diff'
type FileWithOps = { type FileWithOps = {
pathname: FileDiff['pathname'] pathname: FileDiff['pathname']
editable: boolean
operation: FileOperation operation: FileOperation
} }
@ -20,54 +20,70 @@ function getFilesWithOps(
const filesWithOps: FileWithOps[] = [] const filesWithOps: FileWithOps[] = []
if (updateForToV) { if (updateForToV) {
const filesByPathname = new Map<string, FileDiff>()
for (const file of files) {
const pathname = fileFinalPathname(file)
filesByPathname.set(pathname, file)
}
const isEditable = (pathname: string) => {
const fileDiff = filesByPathname.get(pathname)
if (!fileDiff) {
return false
}
return isFileEditable(fileDiff)
}
for (const pathname of updateForToV.pathnames) { for (const pathname of updateForToV.pathnames) {
filesWithOps.push({ filesWithOps.push({
pathname, pathname,
editable: isEditable(pathname),
operation: 'edited', operation: 'edited',
}) })
} }
for (const op of updateForToV.project_ops) { for (const op of updateForToV.project_ops) {
let fileWithOps: Nullable<FileWithOps> = null let pathAndOp: Nullable<Pick<FileWithOps, 'pathname' | 'operation'>> =
null
if (op.add) { if (op.add) {
fileWithOps = { pathAndOp = {
pathname: op.add.pathname, pathname: op.add.pathname,
operation: 'added', operation: 'added',
} }
} else if (op.remove) { } else if (op.remove) {
fileWithOps = { pathAndOp = {
pathname: op.remove.pathname, pathname: op.remove.pathname,
operation: 'removed', operation: 'removed',
} }
} else if (op.rename) { } else if (op.rename) {
fileWithOps = { pathAndOp = {
pathname: op.rename.newPathname, pathname: op.rename.newPathname,
operation: 'renamed', operation: 'renamed',
} }
} }
if (fileWithOps !== null) { if (pathAndOp !== null) {
filesWithOps.push(fileWithOps) filesWithOps.push({
editable: isEditable(pathAndOp.pathname),
...pathAndOp,
})
} }
} }
} }
return filesWithOps return filesWithOps
} else { } else {
const filesWithOps = _.reduce( const filesWithOps = files.reduce((curFilesWithOps, file) => {
files, if ('operation' in file) {
(curFilesWithOps, file) => { curFilesWithOps.push({
if ('operation' in file) { pathname: file.pathname,
curFilesWithOps.push({ editable: isFileEditable(file),
pathname: file.pathname, operation: file.operation,
operation: file.operation, })
}) }
} return curFilesWithOps
return curFilesWithOps }, <FileWithOps[]>[])
},
<FileWithOps[]>[]
)
return filesWithOps return filesWithOps
} }
@ -86,44 +102,25 @@ export function autoSelectFile(
comparing: boolean, comparing: boolean,
updateForToV: LoadedUpdate | undefined updateForToV: LoadedUpdate | undefined
): FileDiff { ): FileDiff {
let fileToSelect: Nullable<FileDiff> = null
const filesWithOps = getFilesWithOps(files, toV, comparing, updateForToV) const filesWithOps = getFilesWithOps(files, toV, comparing, updateForToV)
for (const opType of orderedOpTypes) { for (const opType of orderedOpTypes) {
const fileWithMatchingOpType = _.find(filesWithOps, { const fileWithMatchingOpType = filesWithOps.find(
operation: opType, file => file.operation === opType && file.editable
}) )
if (fileWithMatchingOpType != null) { if (fileWithMatchingOpType) {
fileToSelect = const fileToSelect = files.find(
_.find( file => fileFinalPathname(file) === fileWithMatchingOpType.pathname
files, )
file => fileFinalPathname(file) === fileWithMatchingOpType.pathname if (fileToSelect) {
) ?? null return fileToSelect
break
}
}
if (!fileToSelect) {
const mainFile = _.find(files, function (file) {
return /main\.tex$/.test(file.pathname)
})
if (mainFile) {
fileToSelect = mainFile
} else {
const anyTeXFile = _.find(files, function (file) {
return /\.tex$/.test(file.pathname)
})
if (anyTeXFile) {
fileToSelect = anyTeXFile
} else {
fileToSelect = files[0]
} }
} }
} }
return fileToSelect return (
files.find(file => /main\.tex$/.test(file.pathname)) ||
files.find(file => /\.tex$/.test(file.pathname)) ||
files[0]
)
} }

View file

@ -1,4 +1,9 @@
import type { FileDiff, FileRemoved, FileRenamed } from '../services/types/file' import type {
FileDiff,
FileRemoved,
FileRenamed,
FileWithEditable,
} from '../services/types/file'
export function isFileRenamed(fileDiff: FileDiff): fileDiff is FileRenamed { export function isFileRenamed(fileDiff: FileDiff): fileDiff is FileRenamed {
return (fileDiff as FileRenamed).operation === 'renamed' return (fileDiff as FileRenamed).operation === 'renamed'
@ -8,6 +13,18 @@ export function isFileRemoved(fileDiff: FileDiff): fileDiff is FileRemoved {
return (fileDiff as FileRemoved).operation === 'removed' return (fileDiff as FileRemoved).operation === 'removed'
} }
export function fileFinalPathname(fileDiff: FileDiff) { function isFileWithEditable(fileDiff: FileDiff): fileDiff is FileWithEditable {
return isFileRenamed(fileDiff) ? fileDiff.newPathname : fileDiff.pathname return 'editable' in (fileDiff as FileWithEditable)
}
export function isFileEditable(fileDiff: FileDiff) {
return isFileWithEditable(fileDiff)
? fileDiff.editable
: fileDiff.operation === 'edited'
}
export function fileFinalPathname(fileDiff: FileDiff) {
return (
(isFileRenamed(fileDiff) ? fileDiff.newPathname : null) || fileDiff.pathname
)
} }

View file

@ -1,6 +1,6 @@
import _ from 'lodash' import _ from 'lodash'
import type { FileDiff, FileRenamed } from '../services/types/file' import type { FileDiff, FileRenamed } from '../services/types/file'
import { isFileRemoved } from './file-diff' import { isFileEditable, isFileRemoved } from './file-diff'
export type FileTreeEntity = { export type FileTreeEntity = {
name?: string name?: string
@ -37,6 +37,7 @@ export function reducePathsToTree(
type: 'folder', type: 'folder',
children: <FileTreeEntity[]>[], children: <FileTreeEntity[]>[],
pathname: pathPart, pathname: pathPart,
editable: false,
} }
currentFileTreeLocation.push(fileTreeEntity) currentFileTreeLocation.push(fileTreeEntity)
@ -68,17 +69,12 @@ export function fileTreeDiffToFileTreeData(
if (file.type === 'file') { if (file.type === 'file') {
const deletedAtV = isFileRemoved(file) ? file.deletedAtV : undefined const deletedAtV = isFileRemoved(file) ? file.deletedAtV : undefined
let newDoc: HistoryDoc = { const newDoc: HistoryDoc = {
pathname: file.pathname ?? '', pathname: file.pathname ?? '',
name: file.name ?? '', name: file.name ?? '',
deletedAtV, deletedAtV,
} editable: isFileEditable(file),
operation: 'operation' in file ? file.operation : undefined,
if ('operation' in file) {
newDoc = {
...newDoc,
operation: file.operation,
}
} }
docs.push(newDoc) docs.push(newDoc)
@ -115,5 +111,6 @@ export function renamePathnameKey(file: FileRenamed): FileRenamed {
oldPathname: file.pathname, oldPathname: file.pathname,
pathname: file.newPathname as string, pathname: file.newPathname as string,
operation: file.operation, operation: file.operation,
editable: file.editable,
} }
} }

View file

@ -43,13 +43,16 @@ describe('history toolbar', function () {
}, },
{ {
pathname: 'sample.bib', pathname: 'sample.bib',
editable: true,
}, },
{ {
pathname: 'frog.jpg', pathname: 'frog.jpg',
editable: false,
}, },
], ],
selectedFile: { selectedFile: {
pathname: 'main.tex', pathname: 'main.tex',
editable: true,
}, },
} }
@ -83,18 +86,22 @@ describe('history toolbar', function () {
{ {
pathname: 'main.tex', pathname: 'main.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
{ {
pathname: 'sample.bib', pathname: 'sample.bib',
operation: 'added', operation: 'added',
editable: true,
}, },
{ {
pathname: 'frog.jpg', pathname: 'frog.jpg',
operation: 'added', operation: 'added',
editable: false,
}, },
], ],
selectedFile: { selectedFile: {
pathname: 'main.tex', pathname: 'main.tex',
editable: true,
}, },
} }

View file

@ -23,18 +23,23 @@ describe('autoSelectFile', function () {
const files: FileDiff[] = [ const files: FileDiff[] = [
{ {
pathname: 'main.tex', pathname: 'main.tex',
editable: true,
}, },
{ {
pathname: 'sample.bib', pathname: 'sample.bib',
editable: true,
}, },
{ {
pathname: 'frog.jpg', pathname: 'frog.jpg',
editable: false,
}, },
{ {
pathname: 'newfile5.tex', pathname: 'newfile5.tex',
editable: true,
}, },
{ {
pathname: 'newfolder1/newfolder2/newfile2.tex', pathname: 'newfolder1/newfolder2/newfile2.tex',
editable: true,
}, },
{ {
pathname: 'newfolder1/newfile10.tex', pathname: 'newfolder1/newfile10.tex',
@ -276,18 +281,22 @@ describe('autoSelectFile', function () {
{ {
pathname: 'main.tex', pathname: 'main.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
{ {
pathname: 'sample.bib', pathname: 'sample.bib',
operation: 'added', operation: 'added',
editable: true,
}, },
{ {
pathname: 'frog.jpg', pathname: 'frog.jpg',
operation: 'added', operation: 'added',
editable: false,
}, },
{ {
pathname: 'newfile1.tex', pathname: 'newfile1.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
] ]
@ -347,17 +356,21 @@ describe('autoSelectFile', function () {
pathname: 'main.tex', pathname: 'main.tex',
operation: 'removed', operation: 'removed',
deletedAtV: 6, deletedAtV: 6,
editable: true,
}, },
{ {
pathname: 'sample.bib', pathname: 'sample.bib',
editable: true,
}, },
{ {
pathname: 'main2.tex', pathname: 'main2.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
{ {
pathname: 'main3.tex', pathname: 'main3.tex',
operation: 'added', operation: 'added',
editable: true,
}, },
] ]
@ -446,18 +459,22 @@ describe('autoSelectFile', function () {
const files: FileDiff[] = [ const files: FileDiff[] = [
{ {
pathname: 'main.tex', pathname: 'main.tex',
editable: true,
}, },
{ {
pathname: 'sample.bib', pathname: 'sample.bib',
editable: true,
}, },
{ {
pathname: 'frog.jpg', pathname: 'frog.jpg',
editable: false,
}, },
{ {
pathname: 'newfolder/maybewillbedeleted.tex', pathname: 'newfolder/maybewillbedeleted.tex',
newPathname: 'newfolder2/maybewillbedeleted.tex', newPathname: 'newfolder2/maybewillbedeleted.tex',
operation: 'removed', operation: 'removed',
deletedAtV: 10, deletedAtV: 10,
editable: true,
}, },
] ]
@ -617,12 +634,15 @@ describe('autoSelectFile', function () {
const files: FileDiff[] = [ const files: FileDiff[] = [
{ {
pathname: 'certainly_not_main.tex', pathname: 'certainly_not_main.tex',
editable: true,
}, },
{ {
pathname: 'newfile.tex', pathname: 'newfile.tex',
editable: true,
}, },
{ {
pathname: 'file2.tex', pathname: 'file2.tex',
editable: true,
}, },
] ]
@ -725,11 +745,13 @@ describe('autoSelectFile', function () {
const files: FileDiff[] = [ const files: FileDiff[] = [
{ {
pathname: 'main.tex', pathname: 'main.tex',
editable: true,
}, },
{ {
pathname: 'original.bib', pathname: 'original.bib',
newPathname: 'new.bib', newPathname: 'new.bib',
operation: 'renamed', operation: 'renamed',
editable: true,
}, },
] ]
@ -792,5 +814,79 @@ describe('autoSelectFile', function () {
expect(pathname).to.equal('new.bib') expect(pathname).to.equal('new.bib')
}) })
it('ignores binary file', function () {
const files: FileDiff[] = [
{
pathname: 'frog.jpg',
editable: false,
operation: 'added',
},
{
pathname: 'main.tex',
editable: true,
},
{
pathname: 'sample.bib',
editable: true,
},
]
const updates: LoadedUpdate[] = [
{
fromV: 4,
toV: 7,
meta: {
users: historyUsers,
start_ts: 1680874742389,
end_ts: 1680874755552,
},
labels: [],
pathnames: [],
project_ops: [
{
add: {
pathname: 'frog.jpg',
},
atV: 5,
},
],
},
{
fromV: 0,
toV: 4,
meta: {
users: historyUsers,
start_ts: 1680861975947,
end_ts: 1680861988442,
},
labels: [],
pathnames: [],
project_ops: [
{
add: {
pathname: 'sample.bib',
},
atV: 1,
},
{
add: {
pathname: 'main.tex',
},
atV: 0,
},
],
},
]
const { pathname } = autoSelectFile(
files,
updates[0].toV,
comparing,
getUpdateForVersion(updates[0].toV, updates)
)
expect(pathname).to.equal('main.tex')
})
}) })
}) })