[chat] clone comment threads when cloning project with ranges (#32852)

* [project-history] add best effort flush when cloning project

* [web] update labels in clone project modal for admins

* [project-history] do not shadow history flush failure

* [web] fix accessible label for 'Add comment' button

* [chat] clone comment threads when cloning project with ranges

GitOrigin-RevId: ef30204c8a94b3d6204d56dcca2f62a46319996b
This commit is contained in:
Jakob Ackermann
2026-04-17 07:44:09 +02:00
committed by Copybot
parent 933c0c2943
commit 6117feef1b
9 changed files with 177 additions and 73 deletions

View File

@@ -44,80 +44,95 @@ export function cloneProject(req, res) {
info: { targetProjectId, sourceProjectId },
})
WebApiManager.getHistoryId(targetProjectId, (err, targetHistoryId) => {
if (err) return incrResp.fail(OError.tag(err, 'get target historyId'))
WebApiManager.getHistoryId(sourceProjectId, (err, sourceHistoryId) => {
if (err) return incrResp.fail(OError.tag(err, 'get source historyId'))
incrResp.sendUpdate('cloning full project history data: pending')
HistoryStoreManager.cloneProject(
sourceHistoryId.toString(),
targetHistoryId.toString(),
incrResp.signal(),
(err, stream) => {
if (err) {
incrResp.fail(OError.tag(err, 'clone history-v1 data'))
return
}
// aborted. pipeline() would throw.
if (res.destroyed) {
stream.destroy()
incrResp.fail(new Error('request aborted'))
return
}
// The stream.pipeline callback API does not support options.
Stream.promises.pipeline(stream, res, { end: false }).then(
() => {
incrResp.sendUpdate('clone labels: pending')
LabelsManager.cloneLabels(
sourceProjectId,
targetProjectId,
err => {
if (err) {
incrResp.fail(OError.tag(err, 'clone labels'))
return
}
incrResp.sendUpdate('clone labels: done')
incrResp.sendUpdate('clone resync state: pending')
SyncManager.cloneResyncState(
sourceProjectId,
targetProjectId,
err => {
if (err) {
incrResp.fail(OError.tag(err, 'clone resync state'))
return
}
incrResp.sendUpdate('clone resync state: done')
incrResp.sendUpdate('clone failure record: pending')
ErrorRecorder.cloneFailure(
sourceProjectId,
targetProjectId,
err => {
if (err) {
incrResp.fail(OError.tag(err, 'clone failure'))
return
}
incrResp.sendUpdate('clone failure record: done')
incrResp.sendUpdate('done')
incrResp.end()
}
)
}
)
}
)
},
err => {
incrResp.fail(OError.tag(err, 'stream history-v1 response'))
}
)
}
incrResp.sendUpdate('best effort history flush: pending')
UpdatesProcessor.processUpdatesForProject(sourceProjectId, err => {
if (err) {
logger.warn(
{ err, sourceProjectId },
'failed to flush during history clone'
)
incrResp.sendUpdate(
'best effort history flush: failed, a resync will be required'
)
} else {
incrResp.sendUpdate('best effort history flush: done')
}
WebApiManager.getHistoryId(targetProjectId, (err, targetHistoryId) => {
if (err) return incrResp.fail(OError.tag(err, 'get target historyId'))
WebApiManager.getHistoryId(sourceProjectId, (err, sourceHistoryId) => {
if (err) return incrResp.fail(OError.tag(err, 'get source historyId'))
incrResp.sendUpdate('cloning full project history data: pending')
HistoryStoreManager.cloneProject(
sourceHistoryId.toString(),
targetHistoryId.toString(),
incrResp.signal(),
(err, stream) => {
if (err) {
incrResp.fail(OError.tag(err, 'clone history-v1 data'))
return
}
// aborted. pipeline() would throw.
if (res.destroyed) {
stream.destroy()
incrResp.fail(new Error('request aborted'))
return
}
// The stream.pipeline callback API does not support options.
Stream.promises.pipeline(stream, res, { end: false }).then(
() => {
incrResp.sendUpdate('clone labels: pending')
LabelsManager.cloneLabels(
sourceProjectId,
targetProjectId,
err => {
if (err) {
incrResp.fail(OError.tag(err, 'clone labels'))
return
}
incrResp.sendUpdate('clone labels: done')
incrResp.sendUpdate('clone resync state: pending')
SyncManager.cloneResyncState(
sourceProjectId,
targetProjectId,
err => {
if (err) {
incrResp.fail(OError.tag(err, 'clone resync state'))
return
}
incrResp.sendUpdate('clone resync state: done')
incrResp.sendUpdate('clone failure record: pending')
ErrorRecorder.cloneFailure(
sourceProjectId,
targetProjectId,
err => {
if (err) {
incrResp.fail(OError.tag(err, 'clone failure'))
return
}
incrResp.sendUpdate('clone failure record: done')
incrResp.sendUpdate('done')
incrResp.end()
}
)
}
)
}
)
},
err => {
incrResp.fail(OError.tag(err, 'stream history-v1 response'))
}
)
}
)
})
})
})
}