Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

RevisionContentResolver.kt
/**
 * Dracon - An IntelliJ-Pijul integration.
 * Copyright 2021 JonathanxD <jhrldev@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.github.jonathanxd.dracon.revision

import com.github.jonathanxd.dracon.coroutine.blocking
import com.github.jonathanxd.dracon.i18n.DraconBundle
import com.github.jonathanxd.dracon.ignore.IgnoreUtil
import com.github.jonathanxd.dracon.pijul.Pijul
import com.github.jonathanxd.dracon.pijul.SuccessStatusCode
import com.github.jonathanxd.dracon.pijul.pijul
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.forEachWithProgress
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.vcs.FilePath
import org.apache.commons.codec.digest.DigestUtils
import java.io.File
import java.io.IOException
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest
import java.util.*
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.exists
import kotlin.io.path.relativeTo

@OptIn(ExperimentalPathApi::class)
fun loadStateInRevision(revisionHash: String,
                        project: Project,
                        root: Path,
                        fileToResolve: FilePath): String {

    val tmpTarget = Paths.get(project.baseDir.path, ".idea", "dracon_diffs", revisionHash)
    if (Files.exists(tmpTarget)) {
        Files.walk(tmpTarget).use { walk ->
            walk.sorted(Comparator.reverseOrder())
                .map(Path::toFile)
                .forEach(File::delete)
        }
    }
    Files.createDirectories(tmpTarget)
    copyFolder(root, root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)

    val revisions = pijul(project).latestRevisionNumber(project, tmpTarget).result

    if (revisions != null && revisions.hash == revisionHash) {
        val rollbackOp = pijul(project).reset(project, tmpTarget)
    } else {
        val resetOp = pijul(project).reset(project, tmpTarget)
        val rollbackOp = pijul(project).rollbackTo(revisionHash, project, tmpTarget)
    }

    val relative = Paths.get(fileToResolve.path).relativeTo(root).toString()
    return Files.readString(tmpTarget.resolve(relative))
}

@OptIn(ExperimentalPathApi::class)
fun loadStateInRevision(revisionHash: String,
                        project: Project,
                        root: Path,
                        filePath: Path): String {
    if (Files.isDirectory(filePath)) {
        // Will always fail for directories.
        return ""
    }
    val tempDir = FileUtilRt.createTempDirectory("dracon_diffs-", revisionHash + UUID.randomUUID().toString())
    val tmpTarget = tempDir.toPath()

    copyFolder(root, root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)

    val revisions = pijul(project).latestRevisionNumber(project, tmpTarget).result

    if (revisions != null && revisions.hash == revisionHash) {
        val rollbackOp = pijul(project).reset(project, tmpTarget)

        if (rollbackOp.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of file $filePath in revision $revisionHash. $rollbackOp")
        }
    } else {
        val resetOp = pijul(project).reset(project, tmpTarget)
        if (resetOp.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of file $filePath in revision $revisionHash during reset. $revisions")
        }

        val rollbackOp = pijul(project).rollbackTo(revisionHash, project, tmpTarget)
        if (rollbackOp.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of file $filePath in revision $revisionHash during unrecord. $rollbackOp")
        }
    }
    val relativeToRoot = filePath.subpath(root.nameCount, filePath.nameCount)
    try {
        return Files.readString(tmpTarget.resolve(relativeToRoot))
    } finally {
        FileUtilRt.delete(tempDir)
    }
}

@OptIn(ExperimentalPathApi::class)
fun loadStateInRevisionForAllFiles(revisionHash: String,
                                   project: Project,
                                   root: Path,
                                   i: ProgressIndicator): Map<Path, ByteArray> {
    val sha1 = DigestUtils.sha1Hex(revisionHash)
    val tempDir = FileUtilRt.createTempDirectory("dracon_diffs-all-", sha1)
    val tmpTarget = tempDir.toPath()
    val pathToRevisionState = mutableMapOf<Path, ByteArray>()

    if (Files.exists(tmpTarget.resolve(".pijul"))) {
        val ls = pijul(project).trackedFiles(project, tmpTarget)

        if (ls.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of all files in revision $revisionHash during reset. $ls")
        }

        i.fraction += 0.0001

        val trackedFiles = ls.result!!

        trackedFiles.forEachWithProgress(i) { it, indic ->
            indic.text2 = DraconBundle.message("index.item.description.text", it.toString())
            if (Files.isRegularFile(it)) {
                pathToRevisionState[it] = Files.readAllBytes(it)
            }

        }

        return pathToRevisionState
    }

    copyFolder(root, root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)

    val revisions = pijul(project).latestRevisionNumber(project, tmpTarget).result

    i.text2 = DraconBundle.message("index.reset.text")

    val reset = pijul(project).reset(project, tmpTarget)

    if (reset.statusCode !is SuccessStatusCode) {
        throw IllegalStateException("Failed to load state of all files in revision $revisionHash during reset. $revisions")
    }

    i.fraction += 0.0001

    if (revisions == null || revisions.hash != revisionHash) {
        i.text2 = DraconBundle.message("index.unrecord.description.text")

        val rollbackOp = pijul(project).rollbackTo(revisionHash, project, tmpTarget)
        if (rollbackOp.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of all files in revision $revisionHash during unrecord. $rollbackOp")
        }

        i.fraction += 0.0001
    }

    i.text2 = DraconBundle.message("index.ls.text")

    val ls = pijul(project).trackedFiles(project, tmpTarget)

    if (ls.statusCode !is SuccessStatusCode) {
        throw IllegalStateException("Failed to load state of all files in revision $revisionHash during reset. $ls")
    }

    i.fraction += 0.0001

    val trackedFiles = ls.result!!

    trackedFiles.forEachWithProgress(i) { it, indic ->
        indic.text2 = DraconBundle.message("index.item.description.text", it.toString())
        if (Files.isRegularFile(it)) {
            pathToRevisionState[it] = Files.readAllBytes(it)
        }

    }

    i.text2 = DraconBundle.message("index.item.description.finish.text")

    try {
        return pathToRevisionState
    } finally {
        //FileUtilRt.delete(tempDir)
    }
}

@OptIn(ExperimentalPathApi::class)
fun loadStateInRevisionForAllFilesForFile(revisionHash: String,
                                   project: Project,
                                   root: Path,
                                          file: Path): ByteArray {
    val sha1 = DigestUtils.sha1Hex(revisionHash)
    val tempDir = FileUtilRt.createTempDirectory("dracon_diffs-all-", sha1)
    val tmpTarget = tempDir.toPath()

    if (Files.exists(tmpTarget.resolve(".pijul"))) {
        val relativeToRoot = file.relativeTo(root)
        val relativeToTmpTarget = tmpTarget.resolve(relativeToRoot)

        if (Files.exists(relativeToTmpTarget)) {
            return Files.readAllBytes(relativeToTmpTarget)
        }
    }

    copyFolder(root, root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)

    val revisions = pijul(project).latestRevisionNumber(project, tmpTarget).result

    val reset = pijul(project).reset(project, tmpTarget)

    if (reset.statusCode !is SuccessStatusCode) {
        throw IllegalStateException("Failed to load state of all files in revision $revisionHash during reset. $revisions")
    }

    if (revisions == null || revisions.hash != revisionHash) {
        val rollbackOp = pijul(project).rollbackTo(revisionHash, project, tmpTarget)
        if (rollbackOp.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of all files in revision $revisionHash during unrecord. $rollbackOp")
        }
    }

    try {
        val relativeToRoot = file.relativeTo(root)
        val relativeToTmpTarget = tmpTarget.resolve(relativeToRoot)

        return if (Files.exists(relativeToTmpTarget)) {
            Files.readAllBytes(relativeToTmpTarget)
        } else {
            ByteArray(0)
        }
    } finally {
        //FileUtilRt.delete(tempDir)
    }
}


@OptIn(ExperimentalPathApi::class)
fun loadStateInEveryRevisionForAllFiles(allRevisions: List<String>,
                                        project: Project,
                                        root: Path,
                                        i: ProgressIndicator): Map<String, Map<Path, ByteArray>> {
    if (allRevisions.isEmpty()) {
        throw IllegalArgumentException("Provided 'allRevisions' argument must not be empty.")
    }

    val sha1 = DigestUtils.sha1Hex(allRevisions.toString())
    val tempDir = FileUtilRt.createTempDirectory("dracon_diffs-all-", sha1)
    val tmpTarget = tempDir.toPath()

    val revisionToPathToState = mutableMapOf<String, MutableMap<Path, ByteArray>>()

    copyFolder(root, root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)

    val revisions = pijul(project).allRevisions(project, tmpTarget)

    if (revisions.statusCode !is SuccessStatusCode) {
        throw IllegalStateException("Failed to load state of all files in revisions '$allRevisions' during all revisions hash retrieval. $revisions")
    }

    val pijulRevisions = revisions.result!!.map { it.hash }
    val indexOfFirstRevision = pijulRevisions.indexOf(allRevisions[0])

    if (indexOfFirstRevision == -1) {
        throw IllegalArgumentException("Could not find revision ${allRevisions[0]} in Pijul repository!")
    } else {
        if (indexOfFirstRevision + allRevisions.size > pijulRevisions.size) {
            throw IllegalArgumentException("There are more revisions to unrecord than the amount of recorded changes in Pijul Repository.")
        } else {
            val revisionSubList = pijulRevisions.subList(indexOfFirstRevision, indexOfFirstRevision + allRevisions.size)
            if (allRevisions != revisionSubList) {
                throw IllegalArgumentException("Revisions to load must sequentially match a sub sequence of revisions in Pijul repository. " +
                        "Changes found in pijul: $revisionSubList. Changes to unrecord: $allRevisions")
            }
        }
    }

    i.text2 = DraconBundle.message("index.reset.text")

    val reset = pijul(project).reset(project, tmpTarget)

    if (reset.statusCode !is SuccessStatusCode) {
        IllegalStateException("Failed to load state of all files in revisions '$allRevisions' during reset. $reset").printStackTrace()
        return revisionToPathToState
    }

    i.fraction += 0.0001

    allRevisions.withIndex().toList().forEachWithProgress(i) { (index, rev), indicator ->
        val pathToRevisionState = revisionToPathToState.computeIfAbsent(rev) { mutableMapOf() }

        indicator.text2 = DraconBundle.message("index.revision.description.text", rev)

        if (index == 0) {
            val rollback = pijul(project).rollbackTo(rev, project, tmpTarget)
            if (rollback.statusCode !is SuccessStatusCode) {
                throw IllegalStateException("Failed to load state of all files in revision '$rev' during rollback. $rollback")
            }
        } else {
            val unrecord = pijul(project).unrecord(project, tmpTarget, rev)
            if (unrecord.statusCode !is SuccessStatusCode) {
                throw IllegalStateException("Failed to load state of all files in revision '$rev' during unrecord. $unrecord")
            }
        }

        indicator.text2 = DraconBundle.message("index.ls.text")

        val ls = pijul(project).trackedFiles(project, tmpTarget)

        if (ls.statusCode !is SuccessStatusCode) {
            throw IllegalStateException("Failed to load state of all files in revision $rev during tracked file listing. $ls")
        }

        indicator.fraction += 0.0001

        val trackedFiles = ls.result!!

        trackedFiles.forEachWithProgress(indicator) { it, indic ->
            indic.text2 = DraconBundle.message("index.item.description.text", it.toString())
            if (Files.isRegularFile(it)) {
                val relativeToRoot = root.resolve(it.relativeTo(tmpTarget))
                pathToRevisionState[relativeToRoot] = Files.readAllBytes(it)
            }
        }
    }

    i.text2 = DraconBundle.message("index.item.description.finish.text")

    try {
        return revisionToPathToState
    } finally {
        //FileUtilRt.delete(tempDir)
    }
}

@OptIn(ExperimentalPathApi::class)
fun loadFileStateInRevision(
    revision: String,
    project: Project,
    root: Path,
    cachePath: Path,
    file: Path,
    i: ProgressIndicator,
    cacheAllRevisions: Boolean = true
): FileStateInCache {
    val dir = resolveDirForRevision(cachePath, revision, createIfNotExists = false)
    val fileRelativeToRoot = file.relativeTo(root)

    if (!Files.exists(dir)) {
        val allRevisions = pijul(project).allRevisions(project, root).result!!.map { it.hash }

        val revisions = if (allRevisions[0] == revision) {
            listOf(revision)
        } else {
            allRevisions.subList(0, allRevisions.indexOf(revision) + 1)
        }


        val cacheMap = createCacheForRevisionFromTo(
            revisions,
            project,
            root,
            cachePath,
            i,
            cacheAllRevisions
        )

        if (!cacheMap.containsKey(revision)) {
            throw IllegalStateException("Failed to load state of all files in revision $revision")
        }
    }

    val fileInCache = dir.resolve(fileRelativeToRoot)
    return if (Files.exists(fileInCache) && Files.isRegularFile(fileInCache)) {
        FileStateInCache(file, fileInCache, false, Files.readAllBytes(fileInCache))
    } else {
        FileStateInCache(file, fileInCache, true, ByteArray(0))
    }
}

data class FileStateInCache(
    val originalPath: Path,
    val pathInCache: Path,
    val deleted: Boolean,
    val content: ByteArray
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as FileStateInCache

        if (originalPath != other.originalPath) return false
        if (pathInCache != other.pathInCache) return false
        if (deleted != other.deleted) return false
        if (!content.contentEquals(other.content)) return false

        return true
    }

    override fun hashCode(): Int {
        var result = originalPath.hashCode()
        result = 31 * result + pathInCache.hashCode()
        result = 31 * result + deleted.hashCode()
        result = 31 * result + content.contentHashCode()
        return result
    }
}

fun resolveDirForRevision(cachePath: Path, revision: String, createIfNotExists: Boolean = true): Path {
    val path = cachePath.resolve(revision)
    if (createIfNotExists)
        Files.createDirectories(path)
    return path
}

@OptIn(ExperimentalPathApi::class)
fun createCacheForRevisionFromTo(allRevisions: List<String>,
                                 project: Project,
                                 root: Path,
                                 cachePath: Path,
                                 i: ProgressIndicator,
                                 cacheAllRevisions: Boolean = true): Map<String, Path> {

    if (allRevisions.isEmpty()) {
        throw IllegalArgumentException("Provided 'allRevisions' argument must not be empty.")
    }

    val revisionsToCache = allRevisions.toMutableList()

    i.text2 = DraconBundle.message("index.check.text")
    val revisionToPath = mutableMapOf<String, Path>()
    var lastFoundCachedRevisionPath: Path? = null
    var lastFoundCachedRevision: String? = null

    for (rev in allRevisions) {
        i.text2 = DraconBundle.message("index.check.rev.text", rev)
        val path = resolveDirForRevision(cachePath, rev, createIfNotExists = false)

        if (path.exists()) {
            revisionsToCache.remove(rev)
            lastFoundCachedRevisionPath = path
            lastFoundCachedRevision = rev
            revisionToPath["rev"] = path
        }
    }

    i.text2 = DraconBundle.message("index.item.description.finish.text")

    if (revisionsToCache.isEmpty()) {
        return revisionToPath
    }

    if (!cacheAllRevisions) {
        val last = revisionsToCache.last()
        revisionsToCache.clear()
        revisionsToCache.add(last)
    }

    val temporaryWorkingDirectory = cachePath.resolve(".tmp-work-dir-" + UUID.randomUUID().toString())

    Files.createDirectories(temporaryWorkingDirectory)

    if (lastFoundCachedRevision != null && lastFoundCachedRevisionPath != null) {
        // Copies last found revision into temporaryWorkingDirectory
        // This prevents from rolling back from revisions that are already cached.
        copyFolder(root, lastFoundCachedRevisionPath, temporaryWorkingDirectory)
    } else {
        // Copy current .pijul working dir into temporaryWorkingDirectory
        copyFolder(root, root, temporaryWorkingDirectory)
    }

    val revisions = pijul(project).allRevisions(project, temporaryWorkingDirectory)

    if (revisions.statusCode !is SuccessStatusCode) {
        throw IllegalStateException("Failed to load state of all files in revisions '$revisionsToCache' during all revisions hash retrieval. $revisions")
    }

    val pijulRevisions = revisions.result!!.map { it.hash }
    val indexOfFirstRevision = pijulRevisions.indexOf(revisionsToCache[0])

    if (indexOfFirstRevision == -1) {
        throw IllegalArgumentException("Could not find revision ${revisionsToCache[0]} in Pijul repository!")
    } else {
        if (indexOfFirstRevision + revisionsToCache.size > pijulRevisions.size) {
            throw IllegalArgumentException("There are more revisions to unrecord than the amount of recorded changes in Pijul Repository.")
        } else {
            val revisionSubList = pijulRevisions.subList(indexOfFirstRevision, indexOfFirstRevision + revisionsToCache.size)
            if (revisionsToCache != revisionSubList) {
                throw IllegalArgumentException("Revisions to load must sequentially match a sub sequence of revisions in Pijul repository. " +
                        "Changes found in pijul: $revisionSubList. Changes to unrecord: $revisionsToCache")
            }
        }
    }

    i.text2 = DraconBundle.message("index.reset.text")

    val reset = pijul(project).reset(project, temporaryWorkingDirectory)

    if (reset.statusCode !is SuccessStatusCode) {
        IllegalStateException("Failed to load state of all files in revisions '$revisionsToCache' during reset. $reset").printStackTrace()
        return revisionToPath
    }

    i.fraction += 0.0001

    revisionsToCache.withIndex().toList().forEachWithProgress(i) { (index, rev), indicator ->
        indicator.text2 = DraconBundle.message("index.revision.description.text", rev)

        val resolvedPathForRevision = resolveDirForRevision(cachePath, rev, createIfNotExists = false)

        if (Files.exists(resolvedPathForRevision)) {
            deleteFilesInsideDirectory(temporaryWorkingDirectory)
            copyFolder(root, resolvedPathForRevision, temporaryWorkingDirectory)
        } else {
            val rollback = pijul(project).rollbackTo(rev, project, temporaryWorkingDirectory)
            if (rollback.statusCode !is SuccessStatusCode) {
                throw IllegalStateException("Failed to load state of all files in revision '$rev' during rollback. $rollback")
            }
            /*if (index == 0) {

            } else {
                val unrecord = pijul(project).unrecord(project, temporaryWorkingDirectory, revisionsToCache[index - 1])
                if (unrecord.statusCode !is SuccessStatusCode) {
                    throw IllegalStateException("Failed to load state of all files in revision '$rev' during unrecord. $unrecord")
                }
            }*/

            indicator.text2 = DraconBundle.message("index.copy.text")

            copyFolder(root, temporaryWorkingDirectory, resolvedPathForRevision)
        }

        revisionToPath[rev] = resolvedPathForRevision
    }

    i.text2 = DraconBundle.message("index.item.description.finish.text")

    try {
        return revisionToPath
    } finally {
        FileUtilRt.delete(temporaryWorkingDirectory.toFile())
    }
}

@Throws(IOException::class)
fun copyFolder(pijulRoot: Path,
               source: Path,
               target: Path,
               vararg options: CopyOption) {
    val copyOptions = arrayOf(*options, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)

    Files.walkFileTree(source, object : SimpleFileVisitor<Path>() {
        @Throws(IOException::class)
        override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
            if (dir.fileName.toString().equals(".idea", ignoreCase = true))
                return FileVisitResult.SKIP_SUBTREE
            if (IgnoreUtil.isIgnored(pijulRoot, dir))
                return FileVisitResult.SKIP_SUBTREE

            Files.createDirectories(target.resolve(source.relativize(dir)))
            return FileVisitResult.CONTINUE
        }

        @Throws(IOException::class)
        override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
            Files.copy(file, target.resolve(source.relativize(file)), *copyOptions)
            return FileVisitResult.CONTINUE
        }
    })
}

fun deleteFilesInsideDirectory(path: Path) {
    if (Files.exists(path)) {
        Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
            override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
                return if (dir != path) FileVisitResult.CONTINUE
                else FileVisitResult.SKIP_SUBTREE
            }

            override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
                Files.delete(file)
                return FileVisitResult.CONTINUE
            }
        })
    }
}