diff --git a/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt b/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt new file mode 100644 index 000000000000..ba0f09710750 --- /dev/null +++ b/platform/vcs-log/impl/test/com/intellij/vcs/log/history/FileHistoryTest.kt @@ -0,0 +1,264 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.vcs.log.history + +import com.intellij.openapi.util.Couple +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.LocalFilePath +import com.intellij.vcs.log.data.index.VcsLogPathsIndex +import com.intellij.vcs.log.data.index.VcsLogPathsIndex.ChangeKind.* +import com.intellij.vcs.log.graph.TestGraphBuilder +import com.intellij.vcs.log.graph.TestPermanentGraphInfo +import com.intellij.vcs.log.graph.api.LinearGraph +import com.intellij.vcs.log.graph.asTestGraphString +import com.intellij.vcs.log.graph.graph +import com.intellij.vcs.log.graph.impl.facade.BaseController +import com.intellij.vcs.log.graph.impl.facade.FilteredController +import gnu.trove.TIntObjectHashMap +import org.junit.Assert +import org.junit.Ignore +import org.junit.Test + +class FileHistoryTest { + + fun LinearGraph.assert(startCommit: Int, startPath: FilePath, fileNamesData: FileNamesData, result: TestGraphBuilder.() -> Unit) { + val permanentGraphInfo = TestPermanentGraphInfo(this) + val baseController = BaseController(permanentGraphInfo) + val filteredController = object : FilteredController(baseController, permanentGraphInfo, fileNamesData.getCommits()) {} + + val historyBuilder = FileHistoryBuilder(startCommit, startPath, fileNamesData) + historyBuilder.accept(filteredController, permanentGraphInfo) + + val expectedResultGraph = graph(result) + val actualResultGraph = filteredController.collapsedGraph.compiledGraph + + Assert.assertEquals(expectedResultGraph.asTestGraphString(true), actualResultGraph.asTestGraphString(true)) + } + + @Test + fun linearHistory() { + val path = LocalFilePath("file.txt", false) + val fileNamesData = FileNamesDataBuilder(path) + .addChange(path, 7, listOf(ADDED), listOf(7)) + .addChange(path, 6, listOf(MODIFIED), listOf(7)) + .addChange(path, 4, listOf(MODIFIED), listOf(5)) + .addChange(path, 2, listOf(REMOVED), listOf(3)) + .addChange(path, 0, listOf(ADDED), listOf(1)) + .build() + + graph { + 0(1) + 1(2) + 2(3) + 3(4) + 4(5) + 5(6) + 6(7) + 7() + }.assert(0, path, fileNamesData) { + 0(2.dot) + 2(4.dot) + 4(6.dot) + 6(7) + 7() + } + } + + @Test + fun historyWithRename() { + val afterPath = LocalFilePath("after.txt", false) + val beforePath = LocalFilePath("before.txt", false) + val fileNamesData = FileNamesDataBuilder(afterPath) + .addChange(beforePath, 6, listOf(ADDED), listOf(7)) + .addChange(beforePath, 4, listOf(REMOVED), listOf(5)) + .addChange(afterPath, 4, listOf(ADDED), listOf(5)) + .addRename(5, 4, beforePath, afterPath) + .addChange(afterPath, 2, listOf(MODIFIED), listOf(3)) + .build() + + graph { + 0(1) + 1(2) + 2(3) + 3(4) + 4(5) + 5(6) + 6(7) + 7() + }.assert(0, afterPath, fileNamesData) { + 2(4.dot) + 4(6.dot) + 6() + } + } + + @Test + fun historyForDeleted() { + val path = LocalFilePath("file.txt", false) + val fileNamesData = FileNamesDataBuilder(path) + .addChange(path, 7, listOf(ADDED), listOf(7)) + .addChange(path, 4, listOf(MODIFIED), listOf(5)) + .addChange(path, 2, listOf(REMOVED), listOf(3)) + .build() + + val graph = graph { + 0(1) + 1(2) + 2(3) + 3(4) + 4(5) + 5(6) + 6(7) + 7() + } + + graph.assert(0, path, fileNamesData) { + 2(4.dot) + 4(7.dot) + 7() + } + graph.assert(4, path, fileNamesData) { + 2(4.dot) + 4(7.dot) + 7() + } + } + + @Test + fun historyWithMerges() { + val path = LocalFilePath("file.txt", false) + val fileNamesData = FileNamesDataBuilder(path) + .addChange(path, 7, listOf(ADDED), listOf(7)) + .addChange(path, 6, listOf(MODIFIED), listOf(7)) + .addChange(path, 4, listOf(NOT_CHANGED, MODIFIED), listOf(6, 5)) + .addChange(path, 3, listOf(MODIFIED), listOf(6)) + .addChange(path, 2, listOf(MODIFIED), listOf(4)) + .addChange(path, 1, listOf(MODIFIED, MODIFIED), listOf(3, 2)) + .addChange(path, 0, listOf(MODIFIED), listOf(1)) + .build() + + graph { + 0(1) + 1(2, 3) + 2(4) + 3(6) + 4(6, 5) + 5(7) + 6(7) + 7() + }.assert(0, path, fileNamesData) { + 0(1) + 1(3, 2) + 2(6.dot) + 3(6) + 6(7) + 7() + } + } + + /** + * Rename happens in one branch, while the other branch only consists of couple of trivial merge commits. + * Refiner walks to the trivial branch first instead of meaningful branch and because of this misses the rename completely. + * Solution would be to drop trivial merges before refining and always walk to the NOT_CHANGED branch. + */ + @Ignore + @Test + fun historyWithUndetectedRename() { + val after = LocalFilePath("after.txt", false) + val before = LocalFilePath("before.txt", false) + val fileNamesData = FileNamesDataBuilder(after) + .addChange(before, 7, listOf(ADDED), listOf(7)) + .addChange(before, 6, listOf(MODIFIED), listOf(7)) + .addChange(before, 5, listOf(MODIFIED), listOf(6)) + + .addChange(before, 4, listOf(REMOVED), listOf(5)) + .addChange(after, 4, listOf(ADDED), listOf(5)) + .addRename(5, 4, before, after) + + .addChange(after, 3, listOf(MODIFIED), listOf(4)) + + .addChange(before, 2, listOf(MODIFIED, NOT_CHANGED), listOf(6, 5)) + .addChange(before, 1, listOf(REMOVED, NOT_CHANGED), listOf(2, 3)) + .addChange(after, 1, listOf(ADDED, NOT_CHANGED), listOf(2, 3)) + // rename is not detected at merge commit 1 + .addChange(after, 0, listOf(MODIFIED), listOf(1)) + .build() + + graph { + 0(1) + 1(2, 3) + 2(6, 5) + 3(4) + 4(5) + 5(6) + 6(7) + 7() + }.assert(0, after, fileNamesData) { + 0(3.dot) + 3(4) + 4(5) + 5(6) + 6(7) + 7() + } + } + + @Test + fun cyclicHistoryTest() { + val aFile = LocalFilePath("a.txt", false) + val bFile = LocalFilePath("b.txt", false) + val fileNamesData = FileNamesDataBuilder(aFile) + .addChange(aFile, 4, listOf(ADDED), listOf(4)) + .addChange(aFile, 2, listOf(REMOVED), listOf(3)) + .addChange(bFile, 2, listOf(ADDED), listOf(3)) + .addRename(3, 2, aFile, bFile) + .addChange(bFile, 0, listOf(REMOVED), listOf(1)) + .addChange(aFile, 0, listOf(ADDED), listOf(1)) + .addRename(1, 0, bFile, aFile) + .build() + + graph { + 0(1) + 1(2) + 2(3) + 3(4) + 4() + }.assert(0, aFile, fileNamesData) { + 0(2.dot) + 2(4.dot) + 4() + } + } +} + +private class FileNamesDataBuilder(private val path: FilePath) { + private val commitsMap: MutableMap>> = mutableMapOf() + private val renamesMap: MutableMap, Couple> = mutableMapOf() + + fun addRename(parent: Int, child: Int, beforePath: FilePath, afterPath: FilePath): FileNamesDataBuilder { + renamesMap[Couple(parent, child)] = Couple(beforePath, afterPath) + return this + } + + fun addChange(path: FilePath, commit: Int, changes: List, parents: List): FileNamesDataBuilder { + commitsMap.getOrPut(path) { TIntObjectHashMap() }.put(commit, parents.zip(changes).toIntObjectMap()) + return this + } + + fun build(): FileNamesData { + return object : FileNamesData(path) { + override fun findRename(parent: Int, child: Int, accept: (Couple) -> Boolean): Couple? { + return renamesMap[Couple(parent, child)] + } + + override fun getAffectedCommits(path: FilePath): TIntObjectHashMap> { + return commitsMap[path] ?: TIntObjectHashMap() + } + } + } +} + +private fun List>.toIntObjectMap(): TIntObjectHashMap { + val result = TIntObjectHashMap() + this.forEach { result.put(it.first, it.second) } + return result +} \ No newline at end of file