Merge pull request #19867 from overleaf/em-hide-history-resync-details

Hide history resync changes

GitOrigin-RevId: a36876ff637d0a4c34150b21f4b4e623dff0ab03
This commit is contained in:
Eric Mc Sween 2024-08-13 08:53:39 -04:00 committed by Copybot
parent 40f3db3772
commit e10478f256
9 changed files with 227 additions and 163 deletions

View file

@ -137,7 +137,7 @@ class UpdateSetBuilder {
} }
for (const op of change.operations) { for (const op of change.operations) {
this.applyOperation(op, timestamp, authors) this.applyOperation(op, timestamp, authors, change.origin)
} }
this.currentUpdate.pathnames = Array.from(this.currentUpdate.pathnames) this.currentUpdate.pathnames = Array.from(this.currentUpdate.pathnames)
@ -146,9 +146,9 @@ class UpdateSetBuilder {
this.version += 1 this.version += 1
} }
applyOperation(op, timestamp, authors) { applyOperation(op, timestamp, authors, origin) {
if (UpdateSetBuilder._isTextOperation(op)) { if (UpdateSetBuilder._isTextOperation(op)) {
this.applyTextOperation(op, timestamp, authors) this.applyTextOperation(op, timestamp, authors, origin)
} else if (UpdateSetBuilder._isRenameOperation(op)) { } else if (UpdateSetBuilder._isRenameOperation(op)) {
this.applyRenameOperation(op, timestamp, authors) this.applyRenameOperation(op, timestamp, authors)
} else if (UpdateSetBuilder._isRemoveFileOperation(op)) { } else if (UpdateSetBuilder._isRemoveFileOperation(op)) {
@ -158,7 +158,7 @@ class UpdateSetBuilder {
} }
} }
applyTextOperation(operation, timestamp, authors) { applyTextOperation(operation, timestamp, authors, origin) {
const { pathname } = operation const { pathname } = operation
if (pathname === '') { if (pathname === '') {
// this shouldn't happen, but we continue to allow the user to see the history // this shouldn't happen, but we continue to allow the user to see the history
@ -180,7 +180,7 @@ class UpdateSetBuilder {
return return
} }
file.applyTextOperation(authors, timestamp, this.version, operation) file.applyTextOperation(authors, timestamp, this.version, operation, origin)
this.currentUpdate.pathnames.add(pathname) this.currentUpdate.pathnames.add(pathname)
} }
@ -285,8 +285,8 @@ class File {
this.operations = [] this.operations = []
} }
applyTextOperation(authors, timestamp, version, operation) { applyTextOperation(authors, timestamp, version, operation, origin) {
this.operations.push({ authors, timestamp, version, operation }) this.operations.push({ authors, timestamp, version, operation, origin })
} }
rename(pathname) { rename(pathname) {
@ -309,13 +309,12 @@ class File {
let initialContent let initialContent
const updates = [] const updates = []
for (let operation of this.operations) { for (const operationInfo of this.operations) {
if (!('textOperation' in operation.operation)) { if (!('textOperation' in operationInfo.operation)) {
// We only care about text operations // We only care about text operations
continue continue
} }
let authors, ops, timestamp, version const { authors, timestamp, version, operation } = operationInfo
;({ authors, timestamp, version, operation } = operation)
// Set the initialContent to the latest version we have before the diff // Set the initialContent to the latest version we have before the diff
// begins. 'version' here refers to the document version as we are // begins. 'version' here refers to the document version as we are
// applying the updates. So we store the content *before* applying the // applying the updates. So we store the content *before* applying the
@ -327,6 +326,7 @@ class File {
) )
} }
let ops
;({ content, ops } = this._convertTextOperation( ;({ content, ops } = this._convertTextOperation(
content, content,
operation, operation,
@ -335,7 +335,7 @@ class File {
// We only need to return the updates between fromVersion and toVersion // We only need to return the updates between fromVersion and toVersion
if (fromVersion <= version && version < toVersion) { if (fromVersion <= version && version < toVersion) {
updates.push({ const update = {
meta: { meta: {
users: authors, users: authors,
start_ts: timestamp.getTime(), start_ts: timestamp.getTime(),
@ -343,7 +343,11 @@ class File {
}, },
v: version, v: version,
op: ops, op: ops,
}) }
if (operationInfo.origin) {
update.meta.origin = operationInfo.origin
}
updates.push(update)
} }
} }

View file

@ -1,18 +1,4 @@
/* eslint-disable import _ from 'lodash'
no-proto,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error' import OError from '@overleaf/o-error'
export class ConsistencyError extends OError {} export class ConsistencyError extends OError {}
@ -26,7 +12,7 @@ export const _mocks = {}
export function buildDiff(initialContent, updates) { export function buildDiff(initialContent, updates) {
let diff = [{ u: initialContent }] let diff = [{ u: initialContent }]
for (const update of Array.from(updates)) { for (const update of updates) {
diff = applyUpdateToDiff(diff, update) diff = applyUpdateToDiff(diff, update)
} }
diff = compressDiff(diff) diff = compressDiff(diff)
@ -35,38 +21,54 @@ export function buildDiff(initialContent, updates) {
_mocks.compressDiff = diff => { _mocks.compressDiff = diff => {
const newDiff = [] const newDiff = []
for (const part of Array.from(diff)) { for (const part of diff) {
const lastPart = newDiff[newDiff.length - 1] const users = part.meta?.users ?? []
if (
lastPart != null && if (part.meta?.origin?.kind === 'history-resync') {
(lastPart.meta != null ? lastPart.meta.user : undefined) != null && // Skip history resync updates. Inserts are converted to unchanged text
(part.meta != null ? part.meta.user : undefined) != null // and deletes are skipped, so that they effectively don't appear in the
) { // diff.
if ( if (part.u != null) {
lastPart.i != null &&
part.i != null &&
lastPart.meta.user.id === part.meta.user.id
) {
lastPart.i += part.i
lastPart.meta.start_ts = Math.min(
lastPart.meta.start_ts,
part.meta.start_ts
)
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
} else if (
lastPart.d != null &&
part.d != null &&
lastPart.meta.user.id === part.meta.user.id
) {
lastPart.d += part.d
lastPart.meta.start_ts = Math.min(
lastPart.meta.start_ts,
part.meta.start_ts
)
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
} else {
newDiff.push(part) newDiff.push(part)
} else if (part.i != null) {
newDiff.push({ u: part.i })
} }
continue
}
if (newDiff.length === 0) {
// If we haven't seen other parts yet, we have nothing to merge.
newDiff.push(part)
continue
}
const lastPart = newDiff[newDiff.length - 1]
const lastUsers = lastPart.meta?.users ?? []
const usersNotInBothParts = _.xor(users, lastUsers)
if (usersNotInBothParts.length > 0) {
// If the set of users in the last part and this part are not the same, we
// can't merge.
newDiff.push(part)
continue
}
if (lastPart.i != null && part.i != null) {
// Merge two inserts
lastPart.i += part.i
lastPart.meta.start_ts = Math.min(
lastPart.meta.start_ts,
part.meta.start_ts
)
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
} else if (lastPart.d != null && part.d != null) {
// Merge two deletes
lastPart.d += part.d
lastPart.meta.start_ts = Math.min(
lastPart.meta.start_ts,
part.meta.start_ts
)
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
} else { } else {
newDiff.push(part) newDiff.push(part)
} }
@ -80,7 +82,6 @@ export function compressDiff(...args) {
export function applyOpToDiff(diff, op, meta) { export function applyOpToDiff(diff, op, meta) {
let consumedDiff let consumedDiff
const position = 0
let remainingDiff = diff.slice() let remainingDiff = diff.slice()
;({ consumedDiff, remainingDiff } = _consumeToOffset(remainingDiff, op.p)) ;({ consumedDiff, remainingDiff } = _consumeToOffset(remainingDiff, op.p))
@ -97,16 +98,16 @@ export function applyOpToDiff(diff, op, meta) {
op, op,
meta meta
)) ))
newDiff.push(...Array.from(consumedDiff || [])) newDiff.push(...(consumedDiff || []))
} }
newDiff.push(...Array.from(remainingDiff || [])) newDiff.push(...(remainingDiff || []))
return newDiff return newDiff
} }
_mocks.applyUpdateToDiff = (diff, update) => { _mocks.applyUpdateToDiff = (diff, update) => {
for (const op of Array.from(update.op)) { for (const op of update.op) {
if (op.broken !== true) { if (op.broken !== true) {
diff = applyOpToDiff(diff, op, update.meta) diff = applyOpToDiff(diff, op, update.meta)
} }

View file

@ -189,10 +189,10 @@ function _summarizeUpdates(updates, labels, existingSummarizedUpdates, toV) {
toV = update.v + 1 toV = update.v + 1
} }
// Skip empty and history-resync updates (only record their version). // Skip empty updates (only record their version). Empty updates are
// Empty updates are updates that only contain comment operations. We don't have a UI for // updates that only contain comment operations. We don't have a UI for
// these yet. // these yet.
if (isUpdateEmpty(update) || isHistoryResyncUpdate(update)) { if (isUpdateEmpty(update)) {
continue continue
} }
@ -282,8 +282,11 @@ function _shouldMergeUpdate(update, summarizedUpdate, labels) {
const updateHasFileOps = update.project_ops.length > 0 const updateHasFileOps = update.project_ops.length > 0
const summarizedUpdateHasTextOps = summarizedUpdate.pathnames.size > 0 const summarizedUpdateHasTextOps = summarizedUpdate.pathnames.size > 0
const summarizedUpdateHasFileOps = summarizedUpdate.project_ops.length > 0 const summarizedUpdateHasFileOps = summarizedUpdate.project_ops.length > 0
const isHistoryResync =
update.meta.origin &&
['history-resync', 'history-migration'].includes(update.meta.origin.kind)
if ( if (
!isHistoryResyncUpdate(update) && !isHistoryResync &&
((updateHasTextOps && summarizedUpdateHasFileOps) || ((updateHasTextOps && summarizedUpdateHasFileOps) ||
(updateHasFileOps && summarizedUpdateHasTextOps)) (updateHasFileOps && summarizedUpdateHasTextOps))
) { ) {
@ -343,10 +346,3 @@ function _mergeUpdate(update, summarizedUpdate) {
function isUpdateEmpty(update) { function isUpdateEmpty(update) {
return update.project_ops.length === 0 && update.pathnames.length === 0 return update.project_ops.length === 0 && update.pathnames.length === 0
} }
function isHistoryResyncUpdate(update) {
return (
update.meta.origin?.kind === 'history-resync' ||
update.meta.origin?.kind === 'history-migration'
)
}

View file

@ -1,16 +1,3 @@
/* eslint-disable
no-return-assign,
no-undef,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import sinon from 'sinon' import sinon from 'sinon'
import { expect } from 'chai' import { expect } from 'chai'
import { strict as esmock } from 'esmock' import { strict as esmock } from 'esmock'
@ -23,11 +10,11 @@ describe('DiffGenerator', function () {
this.ts = Date.now() this.ts = Date.now()
this.user_id = 'mock-user-id' this.user_id = 'mock-user-id'
this.user_id_2 = 'mock-user-id-2' this.user_id_2 = 'mock-user-id-2'
return (this.meta = { this.meta = {
start_ts: this.ts, start_ts: this.ts,
end_ts: this.ts, end_ts: this.ts,
user_id: this.user_id, user_id: this.user_id,
}) }
}) })
describe('buildDiff', function () { describe('buildDiff', function () {
@ -43,18 +30,15 @@ describe('DiffGenerator', function () {
.stub() .stub()
.returns(this.diff) .returns(this.diff)
this.DiffGenerator._mocks.compressDiff = sinon.stub().returns(this.diff) this.DiffGenerator._mocks.compressDiff = sinon.stub().returns(this.diff)
return (this.result = this.DiffGenerator.buildDiff( this.result = this.DiffGenerator.buildDiff(this.content, this.updates)
this.content,
this.updates
))
}) })
it('should return the diff', function () { it('should return the diff', function () {
return this.result.should.deep.equal(this.diff) this.result.should.deep.equal(this.diff)
}) })
it('should build the content into an initial diff', function () { it('should build the content into an initial diff', function () {
return this.DiffGenerator._mocks.applyUpdateToDiff this.DiffGenerator._mocks.applyUpdateToDiff
.calledWith( .calledWith(
[ [
{ {
@ -67,106 +51,127 @@ describe('DiffGenerator', function () {
}) })
it('should apply each update', function () { it('should apply each update', function () {
return Array.from(this.updates).map(update => this.updates.map(update =>
this.DiffGenerator._mocks.applyUpdateToDiff this.DiffGenerator._mocks.applyUpdateToDiff
.calledWith(sinon.match.any, update) .calledWith(sinon.match.any, update)
.should.equal(true) .should.equal(true)
) )
}) })
return it('should compress the diff', function () { it('should compress the diff', function () {
return this.DiffGenerator._mocks.compressDiff this.DiffGenerator._mocks.compressDiff
.calledWith(this.diff) .calledWith(this.diff)
.should.equal(true) .should.equal(true)
}) })
}) })
describe('compressDiff', function () { describe('compressDiff', function () {
describe('with adjacent inserts with the same user_id', function () { describe('with adjacent inserts with the same user id', function () {
return it('should create one update with combined meta data and min/max timestamps', function () { it('should create one update with combined meta data and min/max timestamps', function () {
const diff = this.DiffGenerator.compressDiff([ const diff = this.DiffGenerator.compressDiff([
{ {
i: 'foo', i: 'foo',
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }, meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
}, },
{ {
i: 'bar', i: 'bar',
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }, meta: { start_ts: 5, end_ts: 15, users: [this.user_id] },
}, },
]) ])
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ {
i: 'foobar', i: 'foobar',
meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }, meta: { start_ts: 5, end_ts: 20, users: [this.user_id] },
}, },
]) ])
}) })
}) })
describe('with adjacent inserts with different user_ids', function () { describe('with adjacent inserts with different user ids', function () {
return it('should leave the inserts unchanged', function () { it('should leave the inserts unchanged', function () {
const input = [ const input = [
{ {
i: 'foo', i: 'foo',
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }, meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
}, },
{ {
i: 'bar', i: 'bar',
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }, meta: { start_ts: 5, end_ts: 15, users: [this.user_id_2] },
}, },
] ]
const output = this.DiffGenerator.compressDiff(input) const output = this.DiffGenerator.compressDiff(input)
return expect(output).to.deep.equal(input) expect(output).to.deep.equal(input)
}) })
}) })
describe('with adjacent deletes with the same user_id', function () { describe('with adjacent deletes with the same user id', function () {
return it('should create one update with combined meta data and min/max timestamps', function () { it('should create one update with combined meta data and min/max timestamps', function () {
const diff = this.DiffGenerator.compressDiff([ const diff = this.DiffGenerator.compressDiff([
{ {
d: 'foo', d: 'foo',
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }, meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
}, },
{ {
d: 'bar', d: 'bar',
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }, meta: { start_ts: 5, end_ts: 15, users: [this.user_id] },
}, },
]) ])
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ {
d: 'foobar', d: 'foobar',
meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }, meta: { start_ts: 5, end_ts: 20, users: [this.user_id] },
}, },
]) ])
}) })
}) })
return describe('with adjacent deletes with different user_ids', function () { describe('with adjacent deletes with different user ids', function () {
return it('should leave the deletes unchanged', function () { it('should leave the deletes unchanged', function () {
const input = [ const input = [
{ {
d: 'foo', d: 'foo',
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }, meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
}, },
{ {
d: 'bar', d: 'bar',
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }, meta: { start_ts: 5, end_ts: 15, users: [this.user_id_2] },
}, },
] ]
const output = this.DiffGenerator.compressDiff(input) const output = this.DiffGenerator.compressDiff(input)
return expect(output).to.deep.equal(input) expect(output).to.deep.equal(input)
})
})
describe('with history resync updates', function () {
it('should keep only inserts and mark them as unchanged text', function () {
const input = [
{ u: 'untracked text' },
{
i: 'inserted anonymously',
meta: { origin: { kind: 'history-resync' } },
},
{
d: 'deleted anonymously',
meta: { origin: { kind: 'history-resync' } },
},
]
const output = this.DiffGenerator.compressDiff(input)
expect(output).to.deep.equal([
{ u: 'untracked text' },
{ u: 'inserted anonymously' },
])
}) })
}) })
}) })
return describe('applyUpdateToDiff', function () { describe('applyUpdateToDiff', function () {
describe('an insert', function () { describe('an insert', function () {
it('should insert into the middle of (u)nchanged text', function () { it('should insert into the middle of (u)nchanged text', function () {
const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], { const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
op: [{ p: 3, i: 'baz' }], op: [{ p: 3, i: 'baz' }],
meta: this.meta, meta: this.meta,
}) })
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'foo' }, { u: 'foo' },
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
{ u: 'bar' }, { u: 'bar' },
@ -178,7 +183,7 @@ describe('DiffGenerator', function () {
op: [{ p: 0, i: 'baz' }], op: [{ p: 0, i: 'baz' }],
meta: this.meta, meta: this.meta,
}) })
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
{ u: 'foobar' }, { u: 'foobar' },
]) ])
@ -189,7 +194,7 @@ describe('DiffGenerator', function () {
op: [{ p: 6, i: 'baz' }], op: [{ p: 6, i: 'baz' }],
meta: this.meta, meta: this.meta,
}) })
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'foobar' }, { u: 'foobar' },
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
]) ])
@ -200,19 +205,19 @@ describe('DiffGenerator', function () {
[{ i: 'foobar', meta: this.meta }], [{ i: 'foobar', meta: this.meta }],
{ op: [{ p: 3, i: 'baz' }], meta: this.meta } { op: [{ p: 3, i: 'baz' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ i: 'foo', meta: this.meta }, { i: 'foo', meta: this.meta },
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
{ i: 'bar', meta: this.meta }, { i: 'bar', meta: this.meta },
]) ])
}) })
return it('should not count deletes in the running length total', function () { it('should not count deletes in the running length total', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ d: 'deleted', meta: this.meta }, { u: 'foobar' }], [{ d: 'deleted', meta: this.meta }, { u: 'foobar' }],
{ op: [{ p: 3, i: 'baz' }], meta: this.meta } { op: [{ p: 3, i: 'baz' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ d: 'deleted', meta: this.meta }, { d: 'deleted', meta: this.meta },
{ u: 'foo' }, { u: 'foo' },
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
@ -221,14 +226,14 @@ describe('DiffGenerator', function () {
}) })
}) })
return describe('a delete', function () { describe('a delete', function () {
describe('deleting unchanged text', function () { describe('deleting unchanged text', function () {
it('should delete from the middle of (u)nchanged text', function () { it('should delete from the middle of (u)nchanged text', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ u: 'foobazbar' }], [{ u: 'foobazbar' }],
{ op: [{ p: 3, d: 'baz' }], meta: this.meta } { op: [{ p: 3, d: 'baz' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'foo' }, { u: 'foo' },
{ d: 'baz', meta: this.meta }, { d: 'baz', meta: this.meta },
{ u: 'bar' }, { u: 'bar' },
@ -240,7 +245,7 @@ describe('DiffGenerator', function () {
[{ u: 'foobazbar' }], [{ u: 'foobazbar' }],
{ op: [{ p: 0, d: 'foo' }], meta: this.meta } { op: [{ p: 0, d: 'foo' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ d: 'foo', meta: this.meta }, { d: 'foo', meta: this.meta },
{ u: 'bazbar' }, { u: 'bazbar' },
]) ])
@ -251,18 +256,18 @@ describe('DiffGenerator', function () {
[{ u: 'foobazbar' }], [{ u: 'foobazbar' }],
{ op: [{ p: 6, d: 'bar' }], meta: this.meta } { op: [{ p: 6, d: 'bar' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'foobaz' }, { u: 'foobaz' },
{ d: 'bar', meta: this.meta }, { d: 'bar', meta: this.meta },
]) ])
}) })
return it('should delete across multiple (u)changed text parts', function () { it('should delete across multiple (u)changed text parts', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ u: 'foo' }, { u: 'baz' }, { u: 'bar' }], [{ u: 'foo' }, { u: 'baz' }, { u: 'bar' }],
{ op: [{ p: 2, d: 'obazb' }], meta: this.meta } { op: [{ p: 2, d: 'obazb' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'fo' }, { u: 'fo' },
{ d: 'o', meta: this.meta }, { d: 'o', meta: this.meta },
{ d: 'baz', meta: this.meta }, { d: 'baz', meta: this.meta },
@ -278,7 +283,7 @@ describe('DiffGenerator', function () {
[{ i: 'foobazbar', meta: this.meta }], [{ i: 'foobazbar', meta: this.meta }],
{ op: [{ p: 3, d: 'baz' }], meta: this.meta } { op: [{ p: 3, d: 'baz' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ i: 'foo', meta: this.meta }, { i: 'foo', meta: this.meta },
{ i: 'bar', meta: this.meta }, { i: 'bar', meta: this.meta },
]) ])
@ -289,7 +294,7 @@ describe('DiffGenerator', function () {
[{ i: 'foobazbar', meta: this.meta }], [{ i: 'foobazbar', meta: this.meta }],
{ op: [{ p: 0, d: 'foo' }], meta: this.meta } { op: [{ p: 0, d: 'foo' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }]) expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }])
}) })
it('should delete from the end of (u)nchanged text', function () { it('should delete from the end of (u)nchanged text', function () {
@ -297,15 +302,15 @@ describe('DiffGenerator', function () {
[{ i: 'foobazbar', meta: this.meta }], [{ i: 'foobazbar', meta: this.meta }],
{ op: [{ p: 6, d: 'bar' }], meta: this.meta } { op: [{ p: 6, d: 'bar' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }]) expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }])
}) })
return it('should delete across multiple (u)changed and (i)nserted text parts', function () { it('should delete across multiple (u)changed and (i)nserted text parts', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ u: 'foo' }, { i: 'baz', meta: this.meta }, { u: 'bar' }], [{ u: 'foo' }, { i: 'baz', meta: this.meta }, { u: 'bar' }],
{ op: [{ p: 2, d: 'obazb' }], meta: this.meta } { op: [{ p: 2, d: 'obazb' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'fo' }, { u: 'fo' },
{ d: 'o', meta: this.meta }, { d: 'o', meta: this.meta },
{ d: 'b', meta: this.meta }, { d: 'b', meta: this.meta },
@ -315,12 +320,12 @@ describe('DiffGenerator', function () {
}) })
describe('deleting over existing deletes', function () { describe('deleting over existing deletes', function () {
return it('should delete across multiple (u)changed and (d)deleted text parts', function () { it('should delete across multiple (u)changed and (d)deleted text parts', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ u: 'foo' }, { d: 'baz', meta: this.meta }, { u: 'bar' }], [{ u: 'foo' }, { d: 'baz', meta: this.meta }, { u: 'bar' }],
{ op: [{ p: 2, d: 'ob' }], meta: this.meta } { op: [{ p: 2, d: 'ob' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'fo' }, { u: 'fo' },
{ d: 'o', meta: this.meta }, { d: 'o', meta: this.meta },
{ d: 'baz', meta: this.meta }, { d: 'baz', meta: this.meta },
@ -332,7 +337,7 @@ describe('DiffGenerator', function () {
describe("deleting when the text doesn't match", function () { describe("deleting when the text doesn't match", function () {
it('should throw an error when deleting from the middle of (u)nchanged text', function () { it('should throw an error when deleting from the middle of (u)nchanged text', function () {
return expect(() => expect(() =>
this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], { this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
op: [{ p: 3, d: 'xxx' }], op: [{ p: 3, d: 'xxx' }],
meta: this.meta, meta: this.meta,
@ -341,7 +346,7 @@ describe('DiffGenerator', function () {
}) })
it('should throw an error when deleting from the start of (u)nchanged text', function () { it('should throw an error when deleting from the start of (u)nchanged text', function () {
return expect(() => expect(() =>
this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], { this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
op: [{ p: 0, d: 'xxx' }], op: [{ p: 0, d: 'xxx' }],
meta: this.meta, meta: this.meta,
@ -349,8 +354,8 @@ describe('DiffGenerator', function () {
).to.throw(this.DiffGenerator.ConsistencyError) ).to.throw(this.DiffGenerator.ConsistencyError)
}) })
return it('should throw an error when deleting from the end of (u)nchanged text', function () { it('should throw an error when deleting from the end of (u)nchanged text', function () {
return expect(() => expect(() =>
this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], { this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
op: [{ p: 6, d: 'xxx' }], op: [{ p: 6, d: 'xxx' }],
meta: this.meta, meta: this.meta,
@ -360,12 +365,12 @@ describe('DiffGenerator', function () {
}) })
describe('when the last update in the existing diff is a delete', function () { describe('when the last update in the existing diff is a delete', function () {
return it('should insert the new update before the delete', function () { it('should insert the new update before the delete', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ u: 'foo' }, { d: 'bar', meta: this.meta }], [{ u: 'foo' }, { d: 'bar', meta: this.meta }],
{ op: [{ p: 3, i: 'baz' }], meta: this.meta } { op: [{ p: 3, i: 'baz' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ u: 'foo' }, { u: 'foo' },
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
{ d: 'bar', meta: this.meta }, { d: 'bar', meta: this.meta },
@ -373,13 +378,13 @@ describe('DiffGenerator', function () {
}) })
}) })
return describe('when the only update in the existing diff is a delete', function () { describe('when the only update in the existing diff is a delete', function () {
return it('should insert the new update after the delete', function () { it('should insert the new update after the delete', function () {
const diff = this.DiffGenerator.applyUpdateToDiff( const diff = this.DiffGenerator.applyUpdateToDiff(
[{ d: 'bar', meta: this.meta }], [{ d: 'bar', meta: this.meta }],
{ op: [{ p: 0, i: 'baz' }], meta: this.meta } { op: [{ p: 0, i: 'baz' }], meta: this.meta }
) )
return expect(diff).to.deep.equal([ expect(diff).to.deep.equal([
{ d: 'bar', meta: this.meta }, { d: 'bar', meta: this.meta },
{ i: 'baz', meta: this.meta }, { i: 'baz', meta: this.meta },
]) ])

View file

@ -595,12 +595,12 @@ describe('SummarizedUpdatesManager', function () {
makeUpdate({ makeUpdate({
startTs: 20, startTs: 20,
v: 3, v: 3,
origin: { kind: 'origin-a' }, origin: { kind: 'history-resync' },
}), }),
makeUpdate({ makeUpdate({
startTs: 30, startTs: 30,
v: 4, v: 4,
origin: { kind: 'origin-a' }, origin: { kind: 'history-resync' },
}), }),
makeUpdate({ startTs: 40, v: 5 }), makeUpdate({ startTs: 40, v: 5 }),
makeUpdate({ startTs: 50, v: 6 }), makeUpdate({ startTs: 50, v: 6 }),
@ -617,7 +617,7 @@ describe('SummarizedUpdatesManager', function () {
endTs: 40, endTs: 40,
fromV: 3, fromV: 3,
toV: 5, toV: 5,
origin: { kind: 'origin-a' }, origin: { kind: 'history-resync' },
}), }),
makeSummary({ startTs: 0, endTs: 20, fromV: 1, toV: 3 }), makeSummary({ startTs: 0, endTs: 20, fromV: 1, toV: 3 }),
] ]
@ -687,7 +687,13 @@ describe('SummarizedUpdatesManager', function () {
describe('history resync updates', function () { describe('history resync updates', function () {
setupChunks([ setupChunks([
[ [
makeUpdate({ startTs: 0, v: 1 }), makeUpdate({
startTs: 0,
v: 1,
origin: { kind: 'history-resync' },
projectOps: [{ add: { pathname: 'file1.tex' } }],
pathnames: [],
}),
makeUpdate({ makeUpdate({
startTs: 20, startTs: 20,
v: 2, v: 2,
@ -703,13 +709,47 @@ describe('SummarizedUpdatesManager', function () {
v: 3, v: 3,
origin: { kind: 'history-resync' }, origin: { kind: 'history-resync' },
projectOps: [{ add: { pathname: 'file4.tex' } }], projectOps: [{ add: { pathname: 'file4.tex' } }],
pathnames: [],
}), }),
makeUpdate({ startTs: 60, v: 4 }), makeUpdate({
makeUpdate({ startTs: 80, v: 5 }), startTs: 60,
v: 4,
origin: { kind: 'history-resync' },
projectOps: [],
pathnames: ['file1.tex', 'file2.tex', 'file5.tex'],
}),
makeUpdate({
startTs: 80,
v: 5,
origin: { kind: 'history-resync' },
projectOps: [],
pathnames: ['file4.tex'],
}),
makeUpdate({ startTs: 100, v: 6, pathnames: ['file1.tex'] }),
], ],
]) ])
expectSummaries('should skip history-resync updates', {}, [ expectSummaries('should merge creates and edits', {}, [
makeSummary({ startTs: 0, endTs: 90, fromV: 1, toV: 6 }), makeSummary({
startTs: 100,
endTs: 110,
fromV: 6,
toV: 7,
pathnames: ['file1.tex'],
}),
makeSummary({
startTs: 0,
endTs: 90,
fromV: 1,
toV: 6,
origin: { kind: 'history-resync' },
pathnames: ['file5.tex'],
projectOps: [
{ add: { pathname: 'file4.tex' }, atV: 3 },
{ add: { pathname: 'file2.tex' }, atV: 2 },
{ add: { pathname: 'file3.tex' }, atV: 2 },
{ add: { pathname: 'file1.tex' }, atV: 1 },
],
}),
]) ])
}) })
}) })

View file

@ -580,6 +580,7 @@
"history_label_project_current_state": "", "history_label_project_current_state": "",
"history_label_this_version": "", "history_label_this_version": "",
"history_new_label_name": "", "history_new_label_name": "",
"history_resync": "",
"history_view_a11y_description": "", "history_view_a11y_description": "",
"history_view_all": "", "history_view_all": "",
"history_view_labels": "", "history_view_labels": "",

View file

@ -0,0 +1,9 @@
import { useTranslation } from 'react-i18next'
function HistoryResyncChange() {
const { t } = useTranslation()
return <div>{t('history_resync')}</div>
}
export default HistoryResyncChange

View file

@ -20,6 +20,7 @@ import CompareItems from './dropdown/menu-item/compare-items'
import CompareVersionDropdown from './dropdown/compare-version-dropdown' import CompareVersionDropdown from './dropdown/compare-version-dropdown'
import { CompareVersionDropdownContentAllHistory } from './dropdown/compare-version-dropdown-content' import { CompareVersionDropdownContentAllHistory } from './dropdown/compare-version-dropdown-content'
import FileRestoreChange from './file-restore-change' import FileRestoreChange from './file-restore-change'
import HistoryResyncChange from './history-resync-change'
type HistoryVersionProps = { type HistoryVersionProps = {
update: LoadedUpdate update: LoadedUpdate
@ -167,18 +168,24 @@ function HistoryVersion({
))} ))}
{update.meta.origin?.kind === 'file-restore' ? ( {update.meta.origin?.kind === 'file-restore' ? (
<FileRestoreChange origin={update.meta.origin} /> <FileRestoreChange origin={update.meta.origin} />
) : update.meta.origin?.kind === 'history-resync' ? (
<HistoryResyncChange />
) : ( ) : (
<Changes <Changes
pathnames={update.pathnames} pathnames={update.pathnames}
projectOps={update.project_ops} projectOps={update.project_ops}
/> />
)} )}
<MetadataUsersList {update.meta.origin?.kind !== 'history-resync' ? (
users={update.meta.users} <>
origin={update.meta.origin} <MetadataUsersList
currentUserId={currentUserId} users={update.meta.users}
/> origin={update.meta.origin}
<Origin origin={update.meta.origin} /> currentUserId={currentUserId}
/>
<Origin origin={update.meta.origin} />
</>
) : null}
</div> </div>
</HistoryVersionDetails> </HistoryVersionDetails>
</div> </div>

View file

@ -855,6 +855,7 @@
"history_label_project_current_state": "Current state", "history_label_project_current_state": "Current state",
"history_label_this_version": "Label this version", "history_label_this_version": "Label this version",
"history_new_label_name": "New label name", "history_new_label_name": "New label name",
"history_resync": "History resync",
"history_view_a11y_description": "Show all of the project history or only labelled versions.", "history_view_a11y_description": "Show all of the project history or only labelled versions.",
"history_view_all": "All history", "history_view_all": "All history",
"history_view_labels": "Labels", "history_view_labels": "Labels",