diff --git a/CHANGELOG.md b/CHANGELOG.md index 699e8bda..991542b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Fixed + +- Detection of duplicate titles in scanned files (#131) + ### Changed - dependency updates: diff --git a/cli/src/main/kotlin/com/github/zeldigas/text2confl/cli/ClicktExt.kt b/cli/src/main/kotlin/com/github/zeldigas/text2confl/cli/ClicktExt.kt index bf1785a9..6b5b7f89 100644 --- a/cli/src/main/kotlin/com/github/zeldigas/text2confl/cli/ClicktExt.kt +++ b/cli/src/main/kotlin/com/github/zeldigas/text2confl/cli/ClicktExt.kt @@ -8,8 +8,8 @@ import com.github.ajalt.clikt.parameters.options.RawOption import com.github.ajalt.clikt.parameters.options.nullableFlag import com.github.ajalt.mordant.terminal.ConfirmationPrompt import com.github.ajalt.mordant.terminal.StringPrompt +import com.github.zeldigas.text2confl.convert.ConversionException import com.github.zeldigas.text2confl.convert.ConversionFailedException -import com.github.zeldigas.text2confl.convert.FileDoesNotExistException import com.github.zeldigas.text2confl.core.ContentValidationFailedException import com.github.zeldigas.text2confl.core.upload.ContentUploadException import com.github.zeldigas.text2confl.core.upload.InvalidTenantException @@ -33,7 +33,7 @@ fun RawOption.optionalFlag(vararg secondaryNames: String): NullableOption error(ex.message!!) - is FileDoesNotExistException -> error(ex.message!!) + is ConversionException -> error(ex.message!!) is ContentUploadException -> error(ex.message!!) is ConversionFailedException -> { val reason = buildString { diff --git a/convert/src/main/kotlin/com/github/zeldigas/text2confl/convert/Converter.kt b/convert/src/main/kotlin/com/github/zeldigas/text2confl/convert/Converter.kt index af46519b..60bde951 100644 --- a/convert/src/main/kotlin/com/github/zeldigas/text2confl/convert/Converter.kt +++ b/convert/src/main/kotlin/com/github/zeldigas/text2confl/convert/Converter.kt @@ -9,10 +9,7 @@ import com.github.zeldigas.text2confl.convert.markdown.MarkdownFileConverter import java.io.File import java.nio.file.Files import java.nio.file.Path -import kotlin.io.path.exists -import kotlin.io.path.extension -import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.* interface Converter { @@ -22,7 +19,10 @@ interface Converter { } -class FileDoesNotExistException(val file: Path) : RuntimeException("File does not exist: $file") +open class ConversionException(message: String) : RuntimeException(message) + +class FileDoesNotExistException(val file: Path) : ConversionException("File does not exist: $file") +class DuplicateTitlesException(val duplicates: List, message: String) : ConversionException(message) const val DEFAULT_AUTOGEN_BANNER = "Edit source file instead of changing page in Confluence. " + @@ -77,12 +77,32 @@ internal class UniversalConverter( override fun convertDir(dir: Path): List { val documents = scanDocuments(dir) + checkForDuplicates(dir, documents) + return convertFilesInDirectory( dir, ConvertingContext(ReferenceProvider.fromDocuments(dir, documents), conversionParameters, space) ) } + private fun checkForDuplicates(base: Path, documents: Map) { + val duplicates = documents.entries + .groupBy { (_, v) -> v.title } + .entries.asSequence() + .filter { (_, v) -> v.size > 1 } + .map { (title, v) -> title to v.map { it.key.relativeTo(base) }.sorted() } + .map { (title, paths) -> "\"$title\": ${paths.joinToString(", ")}" } + .toList() + if (duplicates.isNotEmpty()) { + throw DuplicateTitlesException( + duplicates, + "Files with duplicate titles detected. Confluence has flat structure and every published page must have unique title.\n${ + duplicates.joinToString("\n") + }" + ) + } + } + private fun scanDocuments(dir: Path) = dir.toFile().walk().filter { it.supported() } .map { diff --git a/convert/src/test/kotlin/com/github/zeldigas/text2confl/convert/UniversalConverterTest.kt b/convert/src/test/kotlin/com/github/zeldigas/text2confl/convert/UniversalConverterTest.kt index 7456f9b2..9f23865c 100644 --- a/convert/src/test/kotlin/com/github/zeldigas/text2confl/convert/UniversalConverterTest.kt +++ b/convert/src/test/kotlin/com/github/zeldigas/text2confl/convert/UniversalConverterTest.kt @@ -17,8 +17,10 @@ import io.mockk.junit5.MockKExtension import io.mockk.mockkStatic import io.mockk.verify import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.io.TempDir +import java.io.File import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -84,6 +86,34 @@ internal class UniversalConverterTest( @Test internal fun `Directory conversion`(@TempDir dir: Path) { + createFileStructure( + dir, + "one.t", + "_two.t", + "three.t", + "three/foo.t", + "three/one.t", + "three/bar.md", + "four.md", + "five.adoc", + "another/hello.t", + "another/foo.t", + ) + every { fileConverter.readHeader(any(), any()) } answers { + val file = it.invocation.args[0] as Path + PageHeader(file.fileName.toString(), emptyMap()) + } + val exception = assertThrows { converter.convertDir(dir) } + assertThat(exception).all { + prop(DuplicateTitlesException::duplicates).hasSize(2) + messageContains("Files with duplicate titles detected. Confluence has flat structure and every published page must have unique title") + messageContains(""""one.t": one.t, three${File.separator}one.t""") + messageContains(""""foo.t": another${File.separator}foo.t, three${File.separator}foo.t""") + } + } + + @Test + internal fun `Detection of documents with duplicate titles`(@TempDir dir: Path) { createFileStructure( dir, "one.t",