From 82ff26ce776e30ea2c3754144e358f555f38b612 Mon Sep 17 00:00:00 2001 From: Sergey Pak Date: Wed, 3 Jul 2024 22:36:20 +0000 Subject: [PATCH] IJPL-13080 DIRC checksum mismatch error in Settings Sync GitOrigin-RevId: ffe305a7eef4187508363cfe0d11ae8e3da66dab --- .../intellij/settingsSync/GitSettingsLog.kt | 89 ++++++++++++++----- .../settingsSync/GitSettingsLogTest.kt | 42 +++++++++ .../MockSettingsSyncIdeMediator.kt | 8 ++ .../settingsSync/SettingsSyncFlowTest.kt | 4 +- 4 files changed, 119 insertions(+), 24 deletions(-) diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/GitSettingsLog.kt b/plugins/settings-sync/src/com/intellij/settingsSync/GitSettingsLog.kt index 5bde4c2482f8..f13103108347 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/GitSettingsLog.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/GitSettingsLog.kt @@ -49,10 +49,12 @@ class GitSettingsLog(private val settingsSyncStorage: Path, private val initialSnapshotProvider: (SettingsSnapshot) -> SettingsSnapshot ) : SettingsLog, Disposable { - private lateinit var repository: Repository + private val FIVE_SECONDS = 5000 + private lateinit var repository: Repository private lateinit var git: Git + private val master: Ref get() = repository.findRef(MASTER_REF_NAME)!! private val ide: Ref get() = repository.findRef(IDE_REF_NAME)!! private val cloud: Ref get() = repository.findRef(CLOUD_REF_NAME)!! @@ -66,10 +68,10 @@ class GitSettingsLog(private val settingsSyncStorage: Path, override fun initialize() { configureJGit() - initRepository() + initAndCheckRepository(canRetry = true) } - private fun initRepository() { + private fun initAndCheckRepository(canRetry: Boolean) { val dotGit: Path = settingsSyncStorage.resolve(".git") repository = FileRepositoryBuilder().setGitDir(dotGit.toFile()).setAutonomous(true).readEnvironment().build() git = Git(repository) @@ -79,17 +81,19 @@ class GitSettingsLog(private val settingsSyncStorage: Path, LOG.info("Initializing new Git repository for Settings Sync at $settingsSyncStorage") repository.create() addInitialCommit(repository) - } else { - val rootLockEntries = dotGit.listDirectoryEntries("*.lock") - val headsLockEntries = (dotGit / "refs" / "heads").listDirectoryEntries("*.lock") - val FIVE_SECONDS = 5000 - for(lock in rootLockEntries + headsLockEntries) { - if (System.currentTimeMillis() - lock.getLastModifiedTime().toMillis() > FIVE_SECONDS) { - FileUtil.delete(lock) - } else { - // Repository is currently in process of operating. - // Shouldn't delete the lock, otherwise we'll damage the repo. Just log instead - LOG.warn("Found new lock (${lock.fileName}) modified ${lock.getLastModifiedTime().toInstant()}. Repo initialization might fail") + } + else { + checkLocks(dotGit) + if (!isRepositoryConsistent()) { + if (!canRetry) { + throw RuntimeException("Settings Sync local repository is corrupted after retry. Will not enable settings sync") + } + LOG.warn("Settings Sync repository is corrupted, removing it") + if (FileUtil.delete(settingsSyncStorage.toFile())) { + initAndCheckRepository(canRetry = false) + } + else { + throw RuntimeException("Settings Sync local repository is corrupted and cannot be deleted. Will not enable settings sync") } } } @@ -101,6 +105,38 @@ class GitSettingsLog(private val settingsSyncStorage: Path, createBranchIfNeeded(IDE_REF_NAME, newRepository) } + private fun checkLocks(dotGit: Path) { + val rootLockEntries = dotGit.listDirectoryEntries("*.lock") + val headsLockEntries = (dotGit / "refs" / "heads").listDirectoryEntries("*.lock") + for (lock in rootLockEntries + headsLockEntries) { + if (System.currentTimeMillis() - lock.getLastModifiedTime().toMillis() > FIVE_SECONDS) { + FileUtil.delete(lock) + } + else { + // Repository is currently in process of operating. + // Shouldn't delete the lock, otherwise we'll damage the repo. Just log instead + LOG.warn("Found new lock (${lock.fileName}) modified ${lock.getLastModifiedTime().toInstant()}. Repo initialization might fail") + } + } + } + + private fun isRepositoryConsistent(): Boolean { + try { + LOG.info("Checking consistency of the repository...") + val commits = git.log().call().toList() + commits.forEach { + LOG.debug("Found local commit ${it.short}") + } + val status = git.status().call() + LOG.info("Local repository has ${commits.size}. Status: ${if (status.isClean) "Clean" else "Dirty"}") + return true + } + catch (ex: Exception) { + LOG.warn("An exception occurred while checking repository consistency. Will delete the repository", ex) + return false + } + } + private fun configureJGit() { GpgSigner.setDefault(MockGpgSigner()) } @@ -127,7 +163,10 @@ class GitSettingsLog(private val settingsSyncStorage: Path, } private fun addInitialCommit(repository: Repository) { - val gitignore = settingsSyncStorage.resolve(".gitignore").createParentDirectories().createFile() + val gitignore = settingsSyncStorage.resolve(".gitignore") + if (!gitignore.exists()) { + gitignore.createParentDirectories().createFile() + } gitignore.write(""" event-log-metadata jdbc-drivers @@ -382,9 +421,9 @@ class GitSettingsLog(private val settingsSyncStorage: Path, conflictingFiles -= pluginJsonPath } if (conflictingFiles.isNotEmpty()) { - if (conflictingFiles.any { !it.startsWith(PathManager.OPTIONS_DIRECTORY) && !it.startsWith(METAINFO_FOLDER) }) + if (conflictingFiles.any { !it.startsWith(PathManager.OPTIONS_DIRECTORY) && !it.startsWith(METAINFO_FOLDER) }) SettingsSyncEventsStatistics.MERGE_CONFLICT_OCCURRED.log(SettingsSyncEventsStatistics.MergeConflictType.SCHEMES) - if (conflictingFiles.any { it.startsWith(PathManager.OPTIONS_DIRECTORY)}) { + if (conflictingFiles.any { it.startsWith(PathManager.OPTIONS_DIRECTORY) }) { SettingsSyncEventsStatistics.MERGE_CONFLICT_OCCURRED.log(SettingsSyncEventsStatistics.MergeConflictType.OPTIONS) } } @@ -431,7 +470,8 @@ class GitSettingsLog(private val settingsSyncStorage: Path, git.add().addFilepattern(".").call() commit(buildCommitMessage(getLocalApplicationInfo(), commitHash), null, false) - } catch (e: GitAPIException) { + } + catch (e: GitAPIException) { NotificationService.getInstance().notifySateRestoreFailed() LOG.error("Failed to restore state to hash = $commitHash", e) } @@ -467,10 +507,12 @@ class GitSettingsLog(private val settingsSyncStorage: Path, return treeIterator } - private fun mergeSettingsProviderFile(settingsProvider: SettingsProvider, - relativePath: String, - ideBranchTip: RevCommit, - cloudBranchTip: RevCommit): String { + private fun mergeSettingsProviderFile( + settingsProvider: SettingsProvider, + relativePath: String, + ideBranchTip: RevCommit, + cloudBranchTip: RevCommit, + ): String { return smartMergeFile(relativePath, ideBranchTip, cloudBranchTip, deserializer = { settingsProvider.deserialize(it) }, serializer = { settingsProvider.serialize(it) }, @@ -492,7 +534,8 @@ class GitSettingsLog(private val settingsSyncStorage: Path, cloudBranchTip: RevCommit, serializer: (T) -> String, deserializer: (String) -> T, - merger: (T?, T, T) -> T): String { + merger: (T?, T, T) -> T, + ): String { val ideContent = getFileContentInBranch(relativePath, ideBranchTip) val ideState = deserializer(ideContent) val cloudContent = getFileContentInBranch(relativePath, cloudBranchTip) diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt index 7ebb2dbea9a3..0a5fedceb87b 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt @@ -10,6 +10,7 @@ import com.intellij.testFramework.TemporaryDirectory import com.intellij.ui.JBAccountInfoService import com.intellij.util.io.createParentDirectories import com.intellij.util.io.write +import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevWalk @@ -496,6 +497,47 @@ internal class GitSettingsLogTest { } } + @Test + @TestFor(issues = ["IJPL-13080"]) + fun `drop and reinit settings sync if cannot init`() { + val editorXml = (configDir / "options" / "editor.xml").createParentDirectories().createFile() + val editorContent = "editorContent" + val state1 = "State 1" + + editorXml.writeText(editorContent) + val settingsLog = initializeGitSettingsLog(editorXml) + + settingsLog.applyIdeState(settingsSnapshot { + fileState("options/editor.xml", state1) + }, "Local changes") + val indexFile = getRepository().indexFile + + val size = 16384 + val zeroBytesArray = ByteArray(size) + indexFile.writeBytes(zeroBytesArray) + assertEquals(size.toLong(), indexFile.length()) + + val editorXmlSync = settingsSyncStorage / "options" / "editor.xml" + assertEquals(state1, editorXmlSync.readText()) + try { + DirCache.read(getRepository()) + } + catch (ex: Exception) { + } + + initializeGitSettingsLog(editorXml) + getRepository().indexFile.length() + assertNotEquals(size, indexFile.length()) + assertTrue(editorXmlSync.exists() && editorXmlSync.readText() == editorContent) + + try { + DirCache.read(getRepository()) + } + catch (ex: Exception) { + fail("Shouldn't fail: ${ex.message}") + } + } + private fun checkUsernameEmail(expectedName: String, expectedEmail: String) { arrayOf>() val editorXml = (configDir / "options" / "editor.xml").createParentDirectories().createFile() diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/MockSettingsSyncIdeMediator.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/MockSettingsSyncIdeMediator.kt index fd0b1dc7dfb7..e1a07b740b9c 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/MockSettingsSyncIdeMediator.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/MockSettingsSyncIdeMediator.kt @@ -11,6 +11,7 @@ internal class MockSettingsSyncIdeMediator : SettingsSyncIdeMediator { internal val files = mutableMapOf() private var exceptionToThrowOnApply: Exception? = null + private var exceptionToThrowOnGetInitial: Exception? = null override suspend fun applyToIde(snapshot: SettingsSnapshot, settings: SettingsSyncState?) { if (exceptionToThrowOnApply != null) { @@ -34,6 +35,9 @@ internal class MockSettingsSyncIdeMediator : SettingsSyncIdeMediator { } override fun getInitialSnapshot(appConfigPath: Path, lastSavedSnapshot: SettingsSnapshot): SettingsSnapshot { + if (exceptionToThrowOnGetInitial != null) { + throw exceptionToThrowOnGetInitial!! + } return getAllFilesFromSettingsAsSnapshot(appConfigPath) } @@ -41,6 +45,10 @@ internal class MockSettingsSyncIdeMediator : SettingsSyncIdeMediator { exceptionToThrowOnApply = exception } + fun throwOnGetInitial(exception: Exception) { + exceptionToThrowOnGetInitial = exception + } + companion object { fun getAllFilesFromSettingsAsSnapshot(appConfigPath: Path): SettingsSnapshot { val settingsSyncStorage = appConfigPath.resolve(SETTINGS_SYNC_STORAGE_FOLDER) diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt index 6715c58399eb..2bcf36d54e91 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt @@ -426,7 +426,9 @@ internal class SettingsSyncFlowTest : SettingsSyncTestBase() { git.add().addFilepattern(".gitignore").call() git.commit().setMessage("init").setNoVerify(true).setSign(false).call() - (dotGit / "index").write("aaaaaaa") + val errorMessage = "Failed to collect initial snapshot" + ideMediator.throwOnGetInitial(RuntimeException(errorMessage)) + LoggedErrorProcessor.executeAndReturnLoggedError { initSettingsSync() }